From 9481380a288be0fce191372d52585acb05cdcc8f Mon Sep 17 00:00:00 2001 From: Mazdak Farrokhzad <twingoow@gmail.com> Date: Tue, 6 Aug 2024 21:06:17 +0200 Subject: [PATCH 01/55] Don't use BuiltinType.I/U128, use AlgebraicType.I/U128 (#116) ## Description of Changes Required to make "SDK Tests" pass in https://github.com/clockworklabs/SpacetimeDB/pull/1559. ## API Not breaking. ## Requires SpacetimeDB PRs - https://github.com/clockworklabs/SpacetimeDB/pull/1559 --------- Co-authored-by: Zeke Foppa <bfops@users.noreply.github.com> --- src/Primitives.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Primitives.cs b/src/Primitives.cs index 9466cca5..93ba5e98 100644 --- a/src/Primitives.cs +++ b/src/Primitives.cs @@ -39,7 +39,7 @@ public void Write(BinaryWriter writer, I128 value) } public AlgebraicType GetAlgebraicType(ITypeRegistrar registrar) => - new AlgebraicType.Builtin(new BuiltinType.I128(new Unit())); + new AlgebraicType.I128(new Unit()); } } @@ -76,7 +76,7 @@ public void Write(BinaryWriter writer, U128 value) } public AlgebraicType GetAlgebraicType(ITypeRegistrar registrar) => - new AlgebraicType.Builtin(new BuiltinType.U128(new Unit())); + new AlgebraicType.U128(new Unit()); } } From ae7c531e79bde2bb8902175432373d2440523dd7 Mon Sep 17 00:00:00 2001 From: Zeke Foppa <196249+bfops@users.noreply.github.com> Date: Tue, 6 Aug 2024 12:28:53 -0700 Subject: [PATCH 02/55] Restore `dotnet pack` functionality (#118) ## Description of Changes Single-line change so that `dotnet pack` stops complaining that nothing was generated. Per @RReverser this brings this package more in line with our other C# packages. ## API Nah nothing breaking. ## Requires SpacetimeDB PRs Nope --------- Co-authored-by: Zeke Foppa <bfops@users.noreply.github.com> --- SpacetimeDB.ClientSDK.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/SpacetimeDB.ClientSDK.csproj b/SpacetimeDB.ClientSDK.csproj index 72a34580..058f8101 100644 --- a/SpacetimeDB.ClientSDK.csproj +++ b/SpacetimeDB.ClientSDK.csproj @@ -5,7 +5,6 @@ <LangVersion>9</LangVersion> <ImplicitUsings>disable</ImplicitUsings> <Nullable>enable</Nullable> - <GeneratePackageOnBuild>True</GeneratePackageOnBuild> <PackageId>SpacetimeDB.ClientSDK</PackageId> <Title>SpacetimeDB SDK</Title> <Authors>jdetter</Authors> From 5e612f37dcd5137284cbd2c140f861eb53958c7c Mon Sep 17 00:00:00 2001 From: Jeremie Pelletier <jeremiep@gmail.com> Date: Wed, 7 Aug 2024 13:39:13 -0400 Subject: [PATCH 03/55] Update DEVELOP.md to reflect new codegen (#119) Also ran it again, committing a few generation changes too :) --- DEVELOP.md | 17 +++------------- src/SpacetimeDB/ClientApi/CallReducer.cs | 5 ++--- src/SpacetimeDB/ClientApi/ClientMessage.cs | 1 + src/SpacetimeDB/ClientApi/DatabaseUpdate.cs | 3 ++- src/SpacetimeDB/ClientApi/EnergyQuanta.cs | 3 ++- src/SpacetimeDB/ClientApi/IdentityToken.cs | 5 ++--- .../ClientApi/InitialSubscription.cs | 5 ++--- src/SpacetimeDB/ClientApi/OneOffQuery.cs | 4 ++-- .../ClientApi/OneOffQueryResponse.cs | 8 +++----- src/SpacetimeDB/ClientApi/OneOffTable.cs | 6 +++--- src/SpacetimeDB/ClientApi/ReducerCallInfo.cs | 6 ++---- src/SpacetimeDB/ClientApi/ServerMessage.cs | 1 + src/SpacetimeDB/ClientApi/Subscribe.cs | 4 ++-- src/SpacetimeDB/ClientApi/TableUpdate.cs | 10 ++++------ .../ClientApi/TransactionUpdate.cs | 7 +------ tools/gen-client-api.bat | 20 +++++++++++++++++++ tools/gen-client-api.sh | 15 ++++++++++++++ 17 files changed, 67 insertions(+), 53 deletions(-) create mode 100644 tools/gen-client-api.bat create mode 100644 tools/gen-client-api.sh diff --git a/DEVELOP.md b/DEVELOP.md index de08bc70..4f24f4d6 100644 --- a/DEVELOP.md +++ b/DEVELOP.md @@ -1,17 +1,6 @@ # Notes for maintainers -## `ClientApi.cs` +## `SpacetimeDB.ClientApi` -The file `ClientApi.cs` is generated by [ProtoBuf](https://protobuf.dev/) -from [the SpacetimeDB client-api-messages proto definition](https://github.com/clockworklabs/SpacetimeDB/blob/master/crates/client-api-messages/protobuf/client_api.proto). -This is not automated. -Whenever the `client_api.proto` changes, you'll have to manually re-run `protoc` to re-generate the definitions. - -```sh -cd ~/clockworklabs/SpacetimeDB/crates/client-api-messages/protobuf -protoc --csharp_out=/absolute/path/to/spacetimedb-csharp-sdk/src \ - ./client_api.proto -``` - -Note that `protoc` cannot understand paths that start with `~`; -you must write the absolute path starting from `/`. +To regenerate this namespace, run the `tools/gen-client-api.sh` or the +`tools/gen-client-api.bat` script. diff --git a/src/SpacetimeDB/ClientApi/CallReducer.cs b/src/SpacetimeDB/ClientApi/CallReducer.cs index 0d065d40..d3f56f38 100644 --- a/src/SpacetimeDB/ClientApi/CallReducer.cs +++ b/src/SpacetimeDB/ClientApi/CallReducer.cs @@ -12,17 +12,16 @@ namespace SpacetimeDB.ClientApi { - [DataContract] [SpacetimeDB.Type] + [DataContract] public partial class CallReducer { [DataMember(Name = "reducer")] public string Reducer = ""; - [DataMember(Name = "args")] public SpacetimeDB.ClientApi.EncodedValue Args = null!; - [DataMember(Name = "request_id")] public uint RequestId; + } } diff --git a/src/SpacetimeDB/ClientApi/ClientMessage.cs b/src/SpacetimeDB/ClientApi/ClientMessage.cs index 4badf680..9cb1c4c7 100644 --- a/src/SpacetimeDB/ClientApi/ClientMessage.cs +++ b/src/SpacetimeDB/ClientApi/ClientMessage.cs @@ -5,6 +5,7 @@ #nullable enable using System; +using SpacetimeDB; namespace SpacetimeDB.ClientApi { diff --git a/src/SpacetimeDB/ClientApi/DatabaseUpdate.cs b/src/SpacetimeDB/ClientApi/DatabaseUpdate.cs index 0cf2567e..245117ad 100644 --- a/src/SpacetimeDB/ClientApi/DatabaseUpdate.cs +++ b/src/SpacetimeDB/ClientApi/DatabaseUpdate.cs @@ -12,11 +12,12 @@ namespace SpacetimeDB.ClientApi { - [DataContract] [SpacetimeDB.Type] + [DataContract] public partial class DatabaseUpdate { [DataMember(Name = "tables")] public System.Collections.Generic.List<SpacetimeDB.ClientApi.TableUpdate> Tables = new(); + } } diff --git a/src/SpacetimeDB/ClientApi/EnergyQuanta.cs b/src/SpacetimeDB/ClientApi/EnergyQuanta.cs index 7cd07b36..ba142cb3 100644 --- a/src/SpacetimeDB/ClientApi/EnergyQuanta.cs +++ b/src/SpacetimeDB/ClientApi/EnergyQuanta.cs @@ -12,11 +12,12 @@ namespace SpacetimeDB.ClientApi { - [DataContract] [SpacetimeDB.Type] + [DataContract] public partial class EnergyQuanta { [DataMember(Name = "quanta")] public U128 Quanta; + } } diff --git a/src/SpacetimeDB/ClientApi/IdentityToken.cs b/src/SpacetimeDB/ClientApi/IdentityToken.cs index b1bbd183..344cbbba 100644 --- a/src/SpacetimeDB/ClientApi/IdentityToken.cs +++ b/src/SpacetimeDB/ClientApi/IdentityToken.cs @@ -12,17 +12,16 @@ namespace SpacetimeDB.ClientApi { - [DataContract] [SpacetimeDB.Type] + [DataContract] public partial class IdentityToken { [DataMember(Name = "identity")] public SpacetimeDB.Identity Identity = new(); - [DataMember(Name = "token")] public string Token = ""; - [DataMember(Name = "address")] public SpacetimeDB.Address Address = new(); + } } diff --git a/src/SpacetimeDB/ClientApi/InitialSubscription.cs b/src/SpacetimeDB/ClientApi/InitialSubscription.cs index ed704238..06ae7ba8 100644 --- a/src/SpacetimeDB/ClientApi/InitialSubscription.cs +++ b/src/SpacetimeDB/ClientApi/InitialSubscription.cs @@ -12,17 +12,16 @@ namespace SpacetimeDB.ClientApi { - [DataContract] [SpacetimeDB.Type] + [DataContract] public partial class InitialSubscription { [DataMember(Name = "database_update")] public SpacetimeDB.ClientApi.DatabaseUpdate DatabaseUpdate = new(); - [DataMember(Name = "request_id")] public uint RequestId; - [DataMember(Name = "total_host_execution_duration_micros")] public ulong TotalHostExecutionDurationMicros; + } } diff --git a/src/SpacetimeDB/ClientApi/OneOffQuery.cs b/src/SpacetimeDB/ClientApi/OneOffQuery.cs index c4625fe5..c103c829 100644 --- a/src/SpacetimeDB/ClientApi/OneOffQuery.cs +++ b/src/SpacetimeDB/ClientApi/OneOffQuery.cs @@ -12,14 +12,14 @@ namespace SpacetimeDB.ClientApi { - [DataContract] [SpacetimeDB.Type] + [DataContract] public partial class OneOffQuery { [DataMember(Name = "message_id")] public byte[] MessageId = Array.Empty<byte>(); - [DataMember(Name = "query_string")] public string QueryString = ""; + } } diff --git a/src/SpacetimeDB/ClientApi/OneOffQueryResponse.cs b/src/SpacetimeDB/ClientApi/OneOffQueryResponse.cs index 4e98289f..35c1d0eb 100644 --- a/src/SpacetimeDB/ClientApi/OneOffQueryResponse.cs +++ b/src/SpacetimeDB/ClientApi/OneOffQueryResponse.cs @@ -12,20 +12,18 @@ namespace SpacetimeDB.ClientApi { - [DataContract] [SpacetimeDB.Type] + [DataContract] public partial class OneOffQueryResponse { [DataMember(Name = "message_id")] public byte[] MessageId = Array.Empty<byte>(); - [DataMember(Name = "error")] public string? Error; - [DataMember(Name = "tables")] - public List<SpacetimeDB.ClientApi.OneOffTable> Tables = new(); - + public System.Collections.Generic.List<SpacetimeDB.ClientApi.OneOffTable> Tables = new(); [DataMember(Name = "total_host_execution_duration_micros")] public ulong TotalHostExecutionDurationMicros; + } } diff --git a/src/SpacetimeDB/ClientApi/OneOffTable.cs b/src/SpacetimeDB/ClientApi/OneOffTable.cs index 84fa81bf..12f7e0f9 100644 --- a/src/SpacetimeDB/ClientApi/OneOffTable.cs +++ b/src/SpacetimeDB/ClientApi/OneOffTable.cs @@ -12,14 +12,14 @@ namespace SpacetimeDB.ClientApi { - [DataContract] [SpacetimeDB.Type] + [DataContract] public partial class OneOffTable { [DataMember(Name = "table_name")] public string TableName = ""; - [DataMember(Name = "rows")] - public List<SpacetimeDB.ClientApi.EncodedValue> Rows = new(); + public System.Collections.Generic.List<SpacetimeDB.ClientApi.EncodedValue> Rows = new(); + } } diff --git a/src/SpacetimeDB/ClientApi/ReducerCallInfo.cs b/src/SpacetimeDB/ClientApi/ReducerCallInfo.cs index 86a67416..44bc8865 100644 --- a/src/SpacetimeDB/ClientApi/ReducerCallInfo.cs +++ b/src/SpacetimeDB/ClientApi/ReducerCallInfo.cs @@ -12,20 +12,18 @@ namespace SpacetimeDB.ClientApi { - [DataContract] [SpacetimeDB.Type] + [DataContract] public partial class ReducerCallInfo { [DataMember(Name = "reducer_name")] public string ReducerName = ""; - [DataMember(Name = "reducer_id")] public uint ReducerId; - [DataMember(Name = "args")] public SpacetimeDB.ClientApi.EncodedValue Args = null!; - [DataMember(Name = "request_id")] public uint RequestId; + } } diff --git a/src/SpacetimeDB/ClientApi/ServerMessage.cs b/src/SpacetimeDB/ClientApi/ServerMessage.cs index 7f4bf714..46b5be3a 100644 --- a/src/SpacetimeDB/ClientApi/ServerMessage.cs +++ b/src/SpacetimeDB/ClientApi/ServerMessage.cs @@ -5,6 +5,7 @@ #nullable enable using System; +using SpacetimeDB; namespace SpacetimeDB.ClientApi { diff --git a/src/SpacetimeDB/ClientApi/Subscribe.cs b/src/SpacetimeDB/ClientApi/Subscribe.cs index e844a347..d5eaba81 100644 --- a/src/SpacetimeDB/ClientApi/Subscribe.cs +++ b/src/SpacetimeDB/ClientApi/Subscribe.cs @@ -12,14 +12,14 @@ namespace SpacetimeDB.ClientApi { - [DataContract] [SpacetimeDB.Type] + [DataContract] public partial class Subscribe { [DataMember(Name = "query_strings")] public System.Collections.Generic.List<string> QueryStrings = new(); - [DataMember(Name = "request_id")] public uint RequestId; + } } diff --git a/src/SpacetimeDB/ClientApi/TableUpdate.cs b/src/SpacetimeDB/ClientApi/TableUpdate.cs index 63400ee6..b9b47ea5 100644 --- a/src/SpacetimeDB/ClientApi/TableUpdate.cs +++ b/src/SpacetimeDB/ClientApi/TableUpdate.cs @@ -12,20 +12,18 @@ namespace SpacetimeDB.ClientApi { - [DataContract] [SpacetimeDB.Type] + [DataContract] public partial class TableUpdate { [DataMember(Name = "table_id")] public uint TableId; - [DataMember(Name = "table_name")] public string TableName = ""; - [DataMember(Name = "deletes")] - public List<SpacetimeDB.ClientApi.EncodedValue> Deletes = new(); - + public System.Collections.Generic.List<SpacetimeDB.ClientApi.EncodedValue> Deletes = new(); [DataMember(Name = "inserts")] - public List<SpacetimeDB.ClientApi.EncodedValue> Inserts = new(); + public System.Collections.Generic.List<SpacetimeDB.ClientApi.EncodedValue> Inserts = new(); + } } diff --git a/src/SpacetimeDB/ClientApi/TransactionUpdate.cs b/src/SpacetimeDB/ClientApi/TransactionUpdate.cs index 5861e9ee..fb71dd9e 100644 --- a/src/SpacetimeDB/ClientApi/TransactionUpdate.cs +++ b/src/SpacetimeDB/ClientApi/TransactionUpdate.cs @@ -18,23 +18,18 @@ public partial class TransactionUpdate { [DataMember(Name = "status")] public SpacetimeDB.ClientApi.UpdateStatus Status = null!; - [DataMember(Name = "timestamp")] public SpacetimeDB.ClientApi.Timestamp Timestamp = new(); - [DataMember(Name = "caller_identity")] public SpacetimeDB.Identity CallerIdentity = new(); - [DataMember(Name = "caller_address")] public SpacetimeDB.Address CallerAddress = new(); - [DataMember(Name = "reducer_call")] public SpacetimeDB.ClientApi.ReducerCallInfo ReducerCall = new(); - [DataMember(Name = "energy_quanta_used")] public SpacetimeDB.ClientApi.EnergyQuanta EnergyQuantaUsed = new(); - [DataMember(Name = "host_execution_duration_micros")] public ulong HostExecutionDurationMicros; + } } diff --git a/tools/gen-client-api.bat b/tools/gen-client-api.bat new file mode 100644 index 00000000..9eb66c49 --- /dev/null +++ b/tools/gen-client-api.bat @@ -0,0 +1,20 @@ +@echo off +setlocal + +if "%CL_HOME%"=="" ( + echo "Variable CL_HOME not set" + exit /b 1 +) + +cd %CL_HOME%\SpacetimeDB\crates\client-api-messages +cargo run --example get_ws_schema > %CL_HOME%/schema.json + +cd %CL_HOME%\SpacetimeDB\crates\cli +cargo run -- generate -l csharp -n SpacetimeDB.ClientApi ^ + --json-module %CL_HOME%\schema.json ^ + -o %CL_HOME%\spacetimedb-csharp-sdk\src\SpacetimeDB\ClientApi + +cd %CL_HOME%\spacetimedb-csharp-sdk\src\SpacetimeDB\ClientApi +del /q _Globals + +del %CL_HOME%\schema.json diff --git a/tools/gen-client-api.sh b/tools/gen-client-api.sh new file mode 100644 index 00000000..91fbe3da --- /dev/null +++ b/tools/gen-client-api.sh @@ -0,0 +1,15 @@ +#!/bin/sh -eu +: $CL_HOME + +cd $CL_HOME/SpacetimeDB/crates/client-api-messages +cargo run --example get_ws_schema > $CL_HOME/schema.json + +cd $CL_HOME/SpacetimeDB/crates/cli +cargo run -- generate -l csharp -n SpacetimeDB.ClientApi \ + --json-module $CL_HOME/schema.json \ + -o $CL_HOME/spacetimedb-csharp-sdk/src/SpacetimeDB/ClientApi + +cd $CL_HOME/spacetimedb-csharp-sdk/src/SpacetimeDB/ClientApi +rm -rf _Globals + +rm -f $CL_HOME/schema.json From 9904a0232ba77cdccc75b769e8a5003dffff6da6 Mon Sep 17 00:00:00 2001 From: Mazdak Farrokhzad <twingoow@gmail.com> Date: Tue, 13 Aug 2024 22:02:39 +0200 Subject: [PATCH 04/55] Nix `Primitives.cs` - types now defined in main repo (#120) ## Description of Changes These types have been moved to the main repo, where they are used by bindings-csharp as well. ## Requires SpacetimeDB PRs - https://github.com/clockworklabs/SpacetimeDB/pull/1477 --------- Co-authored-by: Ingvar Stepanyan <me@rreverser.com> Co-authored-by: Zeke Foppa <bfops@users.noreply.github.com> --- SpacetimeDB.ClientSDK.csproj | 4 +- src/Primitives.cs | 83 ------------------------------------ tests~/SnapshotTests.cs | 16 ++----- 3 files changed, 6 insertions(+), 97 deletions(-) delete mode 100644 src/Primitives.cs diff --git a/SpacetimeDB.ClientSDK.csproj b/SpacetimeDB.ClientSDK.csproj index 058f8101..1c2158b2 100644 --- a/SpacetimeDB.ClientSDK.csproj +++ b/SpacetimeDB.ClientSDK.csproj @@ -16,7 +16,7 @@ <PackageIcon>logo.png</PackageIcon> <PackageReadmeFile>README.md</PackageReadmeFile> <RepositoryUrl>https://github.com/clockworklabs/spacetimedb-csharp-sdk</RepositoryUrl> - <AssemblyVersion>0.11.0</AssemblyVersion> + <AssemblyVersion>0.12.0</AssemblyVersion> <Version>$(AssemblyVersion)</Version> <DefaultItemExcludes>$(DefaultItemExcludes);*~/**</DefaultItemExcludes> <!-- We want to save DLLs for Unity which doesn't support NuGet. --> @@ -24,7 +24,7 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="SpacetimeDB.BSATN.Runtime" Version="0.11.*" /> + <PackageReference Include="SpacetimeDB.BSATN.Runtime" Version="0.12.*" /> <InternalsVisibleTo Include="SpacetimeDB.Tests" /> </ItemGroup> diff --git a/src/Primitives.cs b/src/Primitives.cs deleted file mode 100644 index 93ba5e98..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<I128> - { - 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<I128> - { - 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.I128(new Unit()); - } - } - - public struct U128 : IEquatable<U128> - { - 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<U128> - { - 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.U128(new Unit()); - } - } - -} diff --git a/tests~/SnapshotTests.cs b/tests~/SnapshotTests.cs index 8f722b3c..af4364c1 100644 --- a/tests~/SnapshotTests.cs +++ b/tests~/SnapshotTests.cs @@ -43,23 +43,15 @@ class EnergyQuantaConverter : WriteOnlyJsonConverter<EnergyQuanta> { public override void Write(VerifyJsonWriter writer, EnergyQuanta value) { - Assert.Equal(0uL, value.Quanta.hi); - writer.WriteValue(value.Quanta.lo); + writer.WriteRawValueIfNoStrict(value.Quanta.ToString()); } } - class EncodedValueConverter : WriteOnlyJsonConverter<EncodedValue> + class EncodedValueConverter : WriteOnlyJsonConverter<EncodedValue.Binary> { - public override void Write(VerifyJsonWriter writer, EncodedValue value) + public override void Write(VerifyJsonWriter writer, EncodedValue.Binary value) { - if (value is EncodedValue.Binary(var bytes)) - { - writer.WriteValue(bytes); - } - else - { - throw new InvalidOperationException(); - } + writer.WriteValue(value.Binary_); } } From 7941798b97108a3a20027b5b32180abb4f8b52b6 Mon Sep 17 00:00:00 2001 From: Zeke Foppa <196249+bfops@users.noreply.github.com> Date: Mon, 9 Sep 2024 17:27:55 -0700 Subject: [PATCH 05/55] Copy files from old repo (#127) ## Description of Changes For some reason, this repo was not quite properly synced up with the state of https://github.com/clockworklabs/com.clockworklabs.spacetimedbsdk-archive after https://github.com/clockworklabs/spacetimedb-csharp-sdk/pull/117. It's unclear to me how this happened, since the current state seems to be compatible with 0.11, but the [0.11 release commit/PR](https://github.com/clockworklabs/com.clockworklabs.spacetimedbsdk-archive/commit/382cce05fecbc00caf7c7d060fbde9a2854ad981) also bumped the `package.json` version, which didn't happen in this repo. I re-copied files over. Fortunately, the only real changes were to `package.json` and `README.md`. ## API No breaking changes. ## Requires SpacetimeDB PRs None Co-authored-by: Zeke Foppa <bfops@users.noreply.github.com> --- README.md | 3 +-- package.json | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 189d6612..18cb6f04 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,9 @@ This repository contains the [Unity](https://unity.com/) SDK for SpacetimeDB. Th ## Documentation -The Unity SDK uses the same code as the C# SDK. You can find the documentation for the C# SDK in the [C# SDK Reference](README.dotnet.md). +The Unity SDK uses the same code as the C# SDK. You can find the documentation for the C# SDK in the [C# SDK Reference](https://spacetimedb.com/docs/sdks/c-sharp) There is also a comprehensive Unity tutorial/demo available: - - [Unity Tutorial](https://spacetimedb.com/docs/unity/part-1) Doc - [Unity Demo](https://github.com/clockworklabs/SpacetimeDBUnityTutorial) Repo diff --git a/package.json b/package.json index 8f6aa864..86f04059 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "com.clockworklabs.spacetimedbsdk", "displayName": "SpacetimeDB SDK", - "version": "0.10.0", + "version": "0.11.0", "description": "The SpacetimeDB Client SDK is a software development kit (SDK) designed to interact with and manipulate SpacetimeDB modules..", "keywords": [], "author": { From 65c97adf29be57082d60d2b898a85553aa15bb6a Mon Sep 17 00:00:00 2001 From: SteveGibson <100594800+SteveBoytsun@users.noreply.github.com> Date: Mon, 23 Sep 2024 13:52:50 -0400 Subject: [PATCH 06/55] Logging API (#132) ## Description of Changes *Describe what has been changed, any new features or bug fixes* Changed logging based on [this proposal](https://github.com/clockworklabs/SpacetimeDBPrivate/pull/981) ## API - [x] This is an API breaking change to the SDK *If the API is breaking, please state below what will break* Logging interface is different now. `Logger` has been renamed to `Log`, and its methods have been renamed as well (ex. `Logger.LogError` is now `Log.Error`) ## Requires SpacetimeDB PRs *List any PRs here that are required for this SDK change to work* --------- Co-authored-by: Zeke Foppa <196249+bfops@users.noreply.github.com> Co-authored-by: Zeke Foppa <bfops@users.noreply.github.com> Co-authored-by: Jeremie Pelletier <jeremiep@gmail.com> Co-authored-by: Steve Boytsun <steve@clockwokrlabs.io> Co-authored-by: Ingvar Stepanyan <me@rreverser.com> --- .github/workflows/check-pr-base.yml | 22 ++++++++++++ DEVELOP.md | 4 +-- src/ClientCache.cs | 6 ++-- src/ConsoleLogger.cs | 52 +++++++++++++++++++++-------- src/ISpacetimeDBLogger.cs | 24 ++++++++----- src/SpacetimeDBClient.cs | 50 +++++++++++++-------------- src/UnityDebugLogger.cs | 27 +++++++++++---- src/WebSocket.cs | 2 +- tests~/SnapshotTests.cs | 25 +++++++++++--- tools~/gen-client-api.bat | 20 +++++++++++ tools~/gen-client-api.sh | 15 +++++++++ 11 files changed, 183 insertions(+), 64 deletions(-) create mode 100644 .github/workflows/check-pr-base.yml create mode 100644 tools~/gen-client-api.bat create mode 100644 tools~/gen-client-api.sh diff --git a/.github/workflows/check-pr-base.yml b/.github/workflows/check-pr-base.yml new file mode 100644 index 00000000..73b71c27 --- /dev/null +++ b/.github/workflows/check-pr-base.yml @@ -0,0 +1,22 @@ +name: Git tree checks + +on: + pull_request: + types: [opened, edited, reopened, synchronize] + merge_group: +permissions: read-all + +jobs: + check_base_ref: + name: Only release branches may merge into master + runs-on: ubuntu-latest + steps: + - id: not_based_on_master + if: | + github.event_name == 'pull_request' && + github.event.pull_request.base.ref == 'master' && + ! startsWith(github.event.pull_request.head.ref, 'release/') + run: | + echo 'Only `release/*` branches are allowed to merge into `master`.' + echo 'Maybe your PR should be merging into `staging`?' + exit 1 diff --git a/DEVELOP.md b/DEVELOP.md index 4f24f4d6..2b9ac7d9 100644 --- a/DEVELOP.md +++ b/DEVELOP.md @@ -2,5 +2,5 @@ ## `SpacetimeDB.ClientApi` -To regenerate this namespace, run the `tools/gen-client-api.sh` or the -`tools/gen-client-api.bat` script. +To regenerate this namespace, run the `tools~/gen-client-api.sh` or the +`tools~/gen-client-api.bat` script. diff --git a/src/ClientCache.cs b/src/ClientCache.cs index ec0a7212..3b47f0fe 100644 --- a/src/ClientCache.cs +++ b/src/ClientCache.cs @@ -44,7 +44,7 @@ public bool DeleteEntry(byte[] rowBytes) return true; } - Logger.LogWarning("Deleting value that we don't have (no cached value available)"); + Log.Warn("Deleting value that we don't have (no cached value available)"); return false; } @@ -65,7 +65,7 @@ public void AddTable<T>() if (!tables.TryAdd(name, new TableCache<T>())) { - Logger.LogError($"Table with name already exists: {name}"); + Log.Error($"Table with name already exists: {name}"); } } @@ -76,7 +76,7 @@ public void AddTable<T>() return table; } - Logger.LogError($"We don't know that this table is: {name}"); + Log.Error($"We don't know that this table is: {name}"); return null; } diff --git a/src/ConsoleLogger.cs b/src/ConsoleLogger.cs index 08f2ce06..1711899c 100644 --- a/src/ConsoleLogger.cs +++ b/src/ConsoleLogger.cs @@ -9,10 +9,12 @@ public enum LogLevel { None = 0, Debug = 1, - Warning = 2, - Error = 4, - Exception = 8, - All = Debug | Warning | Error | Exception + Trace = 2, + Info = 4, + Warning = 8, + Error = 16, + Exception = 32, + All = ~0 } LogLevel _logLevel; @@ -21,35 +23,59 @@ public ConsoleLogger(LogLevel logLevel = LogLevel.All) _logLevel = logLevel; } - public void Log(string message) + public void Debug(string message) { if (_logLevel.HasFlag(LogLevel.Debug)) { - Console.WriteLine(message); + Console.WriteLine($"[D] {message}"); } } - public void LogError(string message) + public void Trace(string message) + { + if (_logLevel.HasFlag(LogLevel.Trace)) + { + Console.WriteLine($"[T] {message}"); + } + } + + public void Info(string message) + { + if (_logLevel.HasFlag(LogLevel.Info)) + { + Console.WriteLine($"[I] {message}"); + } + } + + public void Warn(string message) + { + if (_logLevel.HasFlag(LogLevel.Warning)) + { + Console.WriteLine($"[W] {message}"); + } + } + + public void Error(string message) { if (_logLevel.HasFlag(LogLevel.Error)) { - Console.WriteLine($"Error: {message}"); + Console.WriteLine($"[E] {message}"); } } - public void LogException(Exception e) + public void Exception(string message) { if (_logLevel.HasFlag(LogLevel.Exception)) { - Console.WriteLine($"Exception: {e.Message}"); + Console.WriteLine($"[X] {message}"); } } - public void LogWarning(string message) + public void Exception(Exception exception) { - if (_logLevel.HasFlag(LogLevel.Warning)) + if (_logLevel.HasFlag(LogLevel.Exception)) { - Console.WriteLine($"Warning: {message}"); + Console.WriteLine($"[X] {exception}"); } } } diff --git a/src/ISpacetimeDBLogger.cs b/src/ISpacetimeDBLogger.cs index 33d99cda..5223a628 100644 --- a/src/ISpacetimeDBLogger.cs +++ b/src/ISpacetimeDBLogger.cs @@ -4,13 +4,16 @@ namespace SpacetimeDB { public interface ISpacetimeDBLogger { - void Log(string message); - void LogError(string message); - void LogWarning(string message); - void LogException(Exception e); + void Debug(string message); + void Trace(string message); + void Info(string message); + void Warn(string message); + void Error(string message); + void Exception(string message); + void Exception(Exception e); } - public static class Logger + public static class Log { public static ISpacetimeDBLogger Current = @@ -20,9 +23,12 @@ public static class Logger new ConsoleLogger(); #endif - public static void Log(string message) => Current.Log(message); - public static void LogError(string message) => Current.LogError(message); - public static void LogWarning(string message) => Current.LogWarning(message); - public static void LogException(Exception e) => Current.LogException(e); + public static void Debug(string message) => Current.Debug(message); + public static void Trace(string message) => Current.Trace(message); + public static void Info(string message) => Current.Info(message); + public static void Warn(string message) => Current.Warn(message); + public static void Error(string message) => Current.Error(message); + public static void Exception(string message) => Current.Exception(message); + public static void Exception(Exception exception) => Current.Exception(exception); } } diff --git a/src/SpacetimeDBClient.cs b/src/SpacetimeDBClient.cs index e0a9daa4..59773506 100644 --- a/src/SpacetimeDBClient.cs +++ b/src/SpacetimeDBClient.cs @@ -207,13 +207,13 @@ HashSet<byte[]> GetInsertHashSet(System.Type tableType, int tableSize) var table = clientDB.GetTable(tableName); if (table == null) { - Logger.LogError($"Unknown table name: {tableName}"); + Log.Error($"Unknown table name: {tableName}"); continue; } if (update.Deletes.Count != 0) { - Logger.LogWarning("Non-insert during a subscription update!"); + Log.Warn("Non-insert during a subscription update!"); } var hashSet = GetInsertHashSet(table.ClientTableType, initialSubscription.DatabaseUpdate.Tables.Count); @@ -240,7 +240,7 @@ HashSet<byte[]> GetInsertHashSet(System.Type tableType, int tableSize) break; case EncodedValue.Text(var txt): - Logger.LogWarning("JavaScript messages are unsupported."); + Log.Warn("JavaScript messages are unsupported."); break; } } @@ -261,7 +261,7 @@ HashSet<byte[]> GetInsertHashSet(System.Type tableType, int tableSize) var table = clientDB.GetTable(tableName); if (table == null) { - Logger.LogError($"Unknown table name: {tableName}"); + Log.Error($"Unknown table name: {tableName}"); continue; } @@ -279,7 +279,7 @@ HashSet<byte[]> GetInsertHashSet(System.Type tableType, int tableSize) { if ((op.insert is not null && oldOp.insert is not null) || (op.delete is not null && oldOp.delete is not null)) { - Logger.LogWarning($"Update with the same primary key was applied multiple times! tableName={tableName}"); + Log.Warn($"Update with the same primary key was applied multiple times! tableName={tableName}"); // TODO(jdetter): Is this a correctable error? This would be a major error on the // SpacetimeDB side. continue; @@ -315,7 +315,7 @@ HashSet<byte[]> GetInsertHashSet(System.Type tableType, int tableSize) { if ((op.insert is not null && oldOp.insert is not null) || (op.delete is not null && oldOp.delete is not null)) { - Logger.LogWarning($"Update with the same primary key was applied multiple times! tableName={tableName}"); + Log.Warn($"Update with the same primary key was applied multiple times! tableName={tableName}"); // TODO(jdetter): Is this a correctable error? This would be a major error on the // SpacetimeDB side. continue; @@ -348,13 +348,13 @@ HashSet<byte[]> GetInsertHashSet(System.Type tableType, int tableSize) } catch (Exception e) { - Logger.LogException(e); + Log.Exception(e); } break; case UpdateStatus.Failed(var failed): break; case UpdateStatus.OutOfEnergy(var outOfEnergy): - Logger.LogWarning("Failed to execute reducer: out of energy."); + Log.Warn("Failed to execute reducer: out of energy."); break; default: throw new InvalidOperationException(); @@ -368,7 +368,7 @@ HashSet<byte[]> GetInsertHashSet(System.Type tableType, int tableSize) if (!waitingOneOffQueries.Remove(messageId, out var resultSource)) { - Logger.LogError($"Response to unknown one-off-query: {messageId}"); + Log.Error($"Response to unknown one-off-query: {messageId}"); break; } @@ -442,7 +442,7 @@ public void Connect(string? token, string uri, string addressOrName) uri = $"ws://{uri}"; } - Logger.Log($"SpacetimeDBClient: Connecting to {uri} {addressOrName}"); + Log.Info($"SpacetimeDBClient: Connecting to {uri} {addressOrName}"); Task.Run(async () => { try @@ -453,11 +453,11 @@ public void Connect(string? token, string uri, string addressOrName) { if (connectionClosed) { - Logger.Log("Connection closed gracefully."); + Log.Info("Connection closed gracefully."); return; } - Logger.LogException(e); + Log.Exception(e); } }); } @@ -476,7 +476,7 @@ private void OnMessageProcessCompleteUpdate(ReducerEvent? dbEvent, List<DbOp> db } catch (Exception e) { - Logger.LogException(e); + Log.Exception(e); } } } @@ -541,7 +541,7 @@ private void OnMessageProcessCompleteUpdate(ReducerEvent? dbEvent, List<DbOp> db } catch (Exception e) { - Logger.LogException(e); + Log.Exception(e); } } } @@ -566,7 +566,7 @@ private void OnMessageProcessComplete(PreProcessedMessage preProcessed) } catch (Exception e) { - Logger.LogException(e); + Log.Exception(e); } break; case ServerMessage.TransactionUpdate(var transactionUpdate): @@ -581,7 +581,7 @@ private void OnMessageProcessComplete(PreProcessedMessage preProcessed) var requestId = transactionUpdate.ReducerCall.RequestId; if (!stats.ReducerRequestTracker.FinishTrackingRequest(requestId)) { - Logger.LogWarning($"Failed to finish tracking reducer request: {requestId}"); + Log.Warn($"Failed to finish tracking reducer request: {requestId}"); } } OnMessageProcessCompleteUpdate(processed.reducerEvent, dbOps); @@ -591,7 +591,7 @@ private void OnMessageProcessComplete(PreProcessedMessage preProcessed) } catch (Exception e) { - Logger.LogException(e); + Log.Exception(e); } if (processed.reducerEvent is not { } reducerEvent) @@ -607,7 +607,7 @@ private void OnMessageProcessComplete(PreProcessedMessage preProcessed) } catch (Exception e) { - Logger.LogException(e); + Log.Exception(e); } if (!reducerFound && transactionUpdate.Status is UpdateStatus.Failed(var failed)) @@ -618,7 +618,7 @@ private void OnMessageProcessComplete(PreProcessedMessage preProcessed) } catch (Exception e) { - Logger.LogException(e); + Log.Exception(e); } } break; @@ -631,7 +631,7 @@ private void OnMessageProcessComplete(PreProcessedMessage preProcessed) } catch (Exception e) { - Logger.LogException(e); + Log.Exception(e); } break; case ServerMessage.OneOffQueryResponse: @@ -641,7 +641,7 @@ private void OnMessageProcessComplete(PreProcessedMessage preProcessed) } catch (Exception e) { - Logger.LogException(e); + Log.Exception(e); } break; @@ -659,7 +659,7 @@ public void InternalCallReducer<T>(T args) { if (!webSocket.IsConnected) { - Logger.LogError("Cannot call reducer, not connected to server!"); + Log.Error("Cannot call reducer, not connected to server!"); return; } @@ -677,7 +677,7 @@ public void Subscribe(List<string> queries) { if (!webSocket.IsConnected) { - Logger.LogError("Cannot subscribe, not connected to server!"); + Log.Error("Cannot subscribe, not connected to server!"); return; } @@ -714,13 +714,13 @@ public async Task<T[]> OneOffQuery<T>(string query) if (!stats.OneOffRequestTracker.FinishTrackingRequest(requestId)) { - Logger.LogWarning($"Failed to finish tracking one off request: {requestId}"); + Log.Warn($"Failed to finish tracking one off request: {requestId}"); } T[] LogAndThrow(string error) { error = $"While processing one-off-query `{queryString}`, ID {messageId}: {error}"; - Logger.LogError(error); + Log.Error(error); throw new Exception(error); } diff --git a/src/UnityDebugLogger.cs b/src/UnityDebugLogger.cs index e78154bd..114079f0 100644 --- a/src/UnityDebugLogger.cs +++ b/src/UnityDebugLogger.cs @@ -10,25 +10,40 @@ namespace SpacetimeDB { public class UnityDebugLogger : ISpacetimeDBLogger { - public void Log(string message) + public void Debug(string message) { Debug.Log(message); } - public void LogError(string message) + public void Trace(string message) { - Debug.LogError(message); + Debug.Log(message); } - public void LogException(Exception e) + public void Info(string message) { - Debug.LogException(e); + Debug.Log(message); } - public void LogWarning(string message) + public void Warn(string message) { Debug.LogWarning(message); } + + public void Error(string message) + { + Debug.LogError(message); + } + + public void Exception(string message) + { + Debug.LogError(message); + } + + public void Exception(Exception e) + { + Debug.LogException(e); + } } } #endif diff --git a/src/WebSocket.cs b/src/WebSocket.cs index 99445528..73d23e9a 100644 --- a/src/WebSocket.cs +++ b/src/WebSocket.cs @@ -75,7 +75,7 @@ public async Task Connect(string? auth, string host, string nameOrAddress, Addre } catch (Exception ex) { - Logger.LogException(ex); + Log.Exception(ex); if (OnConnectError != null) { var message = ex.Message; diff --git a/tests~/SnapshotTests.cs b/tests~/SnapshotTests.cs index af4364c1..8aeacaa3 100644 --- a/tests~/SnapshotTests.cs +++ b/tests~/SnapshotTests.cs @@ -57,22 +57,37 @@ public override void Write(VerifyJsonWriter writer, EncodedValue.Binary value) class TestLogger(Events events) : ISpacetimeDBLogger { - public void Log(string message) + public void Debug(string message) + { + events.Add("Debug", message); + } + + public void Trace(string message) + { + events.Add("Trace", message); + } + + public void Info(string message) { events.Add("Log", message); } - public void LogWarning(string message) + public void Warn(string message) { events.Add("LogWarning", message); } - public void LogError(string message) + public void Error(string message) { events.Add("LogError", message); } - public void LogException(Exception e) + public void Exception(string message) + { + events.Add("LogException", message); + } + + public void Exception(Exception e) { events.Add("LogException", e.Message); } @@ -238,7 +253,7 @@ public async Task VerifyAllTablesParsed() { var events = new Events(); - Logger.Current = new TestLogger(events); + Log.Current = new TestLogger(events); var client = SpacetimeDBClient.instance; diff --git a/tools~/gen-client-api.bat b/tools~/gen-client-api.bat new file mode 100644 index 00000000..9eb66c49 --- /dev/null +++ b/tools~/gen-client-api.bat @@ -0,0 +1,20 @@ +@echo off +setlocal + +if "%CL_HOME%"=="" ( + echo "Variable CL_HOME not set" + exit /b 1 +) + +cd %CL_HOME%\SpacetimeDB\crates\client-api-messages +cargo run --example get_ws_schema > %CL_HOME%/schema.json + +cd %CL_HOME%\SpacetimeDB\crates\cli +cargo run -- generate -l csharp -n SpacetimeDB.ClientApi ^ + --json-module %CL_HOME%\schema.json ^ + -o %CL_HOME%\spacetimedb-csharp-sdk\src\SpacetimeDB\ClientApi + +cd %CL_HOME%\spacetimedb-csharp-sdk\src\SpacetimeDB\ClientApi +del /q _Globals + +del %CL_HOME%\schema.json diff --git a/tools~/gen-client-api.sh b/tools~/gen-client-api.sh new file mode 100644 index 00000000..91fbe3da --- /dev/null +++ b/tools~/gen-client-api.sh @@ -0,0 +1,15 @@ +#!/bin/sh -eu +: $CL_HOME + +cd $CL_HOME/SpacetimeDB/crates/client-api-messages +cargo run --example get_ws_schema > $CL_HOME/schema.json + +cd $CL_HOME/SpacetimeDB/crates/cli +cargo run -- generate -l csharp -n SpacetimeDB.ClientApi \ + --json-module $CL_HOME/schema.json \ + -o $CL_HOME/spacetimedb-csharp-sdk/src/SpacetimeDB/ClientApi + +cd $CL_HOME/spacetimedb-csharp-sdk/src/SpacetimeDB/ClientApi +rm -rf _Globals + +rm -f $CL_HOME/schema.json From 7bef4483ac9c496a9c7da68a7369aa39acc74e23 Mon Sep 17 00:00:00 2001 From: Jeremie Pelletier <jeremiep@gmail.com> Date: Mon, 30 Sep 2024 21:06:33 -0400 Subject: [PATCH 07/55] c# client SDK (DbConnection + SDK Callbacks) (#131) Implementation of the db connection proposal for C# --------- Co-authored-by: Ingvar Stepanyan <me@rreverser.com> --- SpacetimeDB.ClientSDK.csproj | 2 +- examples~/quickstart/client/Program.cs | 123 +++--- .../client/module_bindings/Message.cs | 28 +- .../module_bindings/SendMessageReducer.cs | 26 +- .../client/module_bindings/SetNameReducer.cs | 26 +- .../quickstart/client/module_bindings/User.cs | 42 +- .../_Globals/SpacetimeDBClient.cs | 164 ++++++-- examples~/quickstart/server/src/lib.rs | 18 +- src/ClientCache.cs | 36 +- src/Event.cs | 38 ++ src/{IDatabaseTable.cs.meta => Event.cs.meta} | 6 +- src/IDatabaseTable.cs | 82 ---- src/Primitives.cs.meta | 11 - src/SpacetimeDBClient.cs | 344 +++++++++------- src/Stubs.cs | 34 -- src/Table.cs | 72 ++++ src/{Stubs.cs.meta => Table.cs.meta} | 4 +- ...otTests.VerifyAllTablesParsed.verified.txt | 367 ++++++++++++------ tests~/SnapshotTests.cs | 45 ++- tests~/VerifyInit.cs | 2 - 20 files changed, 849 insertions(+), 621 deletions(-) create mode 100644 src/Event.cs rename src/{IDatabaseTable.cs.meta => Event.cs.meta} (65%) delete mode 100644 src/IDatabaseTable.cs delete mode 100644 src/Primitives.cs.meta delete mode 100644 src/Stubs.cs create mode 100644 src/Table.cs rename src/{Stubs.cs.meta => Table.cs.meta} (81%) diff --git a/SpacetimeDB.ClientSDK.csproj b/SpacetimeDB.ClientSDK.csproj index 1c2158b2..006a68bf 100644 --- a/SpacetimeDB.ClientSDK.csproj +++ b/SpacetimeDB.ClientSDK.csproj @@ -1,4 +1,4 @@ -<Project Sdk="Microsoft.NET.Sdk"> +<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>netstandard2.1</TargetFramework> diff --git a/examples~/quickstart/client/Program.cs b/examples~/quickstart/client/Program.cs index 056d940e..7a1fb412 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) { @@ -78,7 +86,7 @@ void User_OnUpdate(User oldValue, User newValue, ReducerEvent? dbEvent) void PrintMessage(Message message) { - var sender = User.FindByIdentity(message.Sender); + var sender = conn.RemoteTables.User.FindByIdentity(message.Sender); var senderName = "unknown"; if (sender != null) { @@ -88,44 +96,59 @@ 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.Reducer is not Event<Reducer>.SubscribeApplied) { PrintMessage(insertedValue); } } -void Reducer_OnSetNameEvent(ReducerEvent reducerEvent, string name) +void Reducer_OnSetNameEvent(EventContext ctx, string name) { - if (reducerEvent.Identity == local_identity && reducerEvent.Status is UpdateStatus.Failed) + if (ctx.Reducer is Event<Reducer>.Reducer reducer) { - Console.Write($"Failed to change name to {name}"); + var e = reducer.ReducerEvent; + if (e.CallerIdentity == local_identity && e.Status is Status.Failed(var error)) + { + Console.Write($"Failed to change name to {name}: {error}"); + } } } -void Reducer_OnSendMessageEvent(ReducerEvent reducerEvent, string text) +void Reducer_OnSendMessageEvent(EventContext ctx, string text) { - if (reducerEvent.Identity == local_identity && reducerEvent.Status is UpdateStatus.Failed) + if (ctx.Reducer is Event<Reducer>.Reducer reducer) { - Console.Write($"Failed to send message {text}"); + var e = reducer.ReducerEvent; + if (e.CallerIdentity == local_identity && e.Status is Status.Failed(var error)) + { + Console.Write($"Failed to send message {text}: {error}"); + } } } -void OnConnect() +void OnConnect(Identity identity, string authToken) { - SpacetimeDBClient.instance.Subscribe(new List<string> { "SELECT * FROM User", "SELECT * FROM Message" }); + local_identity = identity; + AuthToken.SaveToken(authToken); + + conn!.Subscribe(new List<string> { "SELECT * FROM User", "SELECT * FROM Message" }); } -void OnIdentityReceived(string authToken, Identity identity, Address _address) +void OnConnectError(WebSocketError? error, string message) { - local_identity = identity; - AuthToken.SaveToken(authToken); + +} + +void OnDisconnect(DbConnection conn, WebSocketCloseStatus? status, WebSocketError? error) +{ + } void PrintMessagesInOrder() { - foreach (Message message in Message.Iter().OrderBy(item => item.Sent)) + foreach (Message message in conn.RemoteTables.Message.Iter().OrderBy(item => item.Sent)) { PrintMessage(message); } @@ -137,29 +160,29 @@ void OnSubscriptionApplied() PrintMessagesInOrder(); } -void onUnhandledReducerError(ReducerEvent reducerEvent) +void onUnhandledReducerError(ReducerEvent<Reducer> reducerEvent) { - Console.WriteLine($"Unhandled reducer error in {reducerEvent.ReducerName}: {reducerEvent.ErrMessage}"); + Console.WriteLine($"Unhandled reducer error in {reducerEvent.Reducer}: {reducerEvent.Status}"); } -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) + try { - SpacetimeDBClient.instance.Update(); + // 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 +215,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..c0e52cf0 100644 --- a/examples~/quickstart/client/module_bindings/Message.cs +++ b/examples~/quickstart/client/module_bindings/Message.cs @@ -14,30 +14,30 @@ namespace SpacetimeDB.Types { [SpacetimeDB.Type] [DataContract] - public partial class Message : SpacetimeDB.DatabaseTable<Message, SpacetimeDB.Types.ReducerEvent> + public partial class Message : IDatabaseRow { [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 static IEnumerable<Message> FilterBySender(SpacetimeDB.Identity value) + public Message( + SpacetimeDB.Identity Sender, + ulong Sent, + string Text + ) { - return Query(x => x.Sender == value); + this.Sender = Sender; + this.Sent = Sent; + this.Text = Text; } - public static IEnumerable<Message> FilterBySent(ulong value) + public Message() { - return Query(x => x.Sent == value); + this.Sender = new(); + this.Text = ""; } - - public static IEnumerable<Message> FilterByText(string value) - { - return Query(x => x.Text == value); - } - - } } diff --git a/examples~/quickstart/client/module_bindings/SendMessageReducer.cs b/examples~/quickstart/client/module_bindings/SendMessageReducer.cs index ffb537b6..45dd29f3 100644 --- a/examples~/quickstart/client/module_bindings/SendMessageReducer.cs +++ b/examples~/quickstart/client/module_bindings/SendMessageReducer.cs @@ -12,32 +12,8 @@ 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); + string IReducerArgs.ReducerName => "send_message"; 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..0f96ad9e 100644 --- a/examples~/quickstart/client/module_bindings/SetNameReducer.cs +++ b/examples~/quickstart/client/module_bindings/SetNameReducer.cs @@ -12,32 +12,8 @@ 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); + string IReducerArgs.ReducerName => "set_name"; 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..a6d6d1b9 100644 --- a/examples~/quickstart/client/module_bindings/User.cs +++ b/examples~/quickstart/client/module_bindings/User.cs @@ -14,47 +14,29 @@ namespace SpacetimeDB.Types { [SpacetimeDB.Type] [DataContract] - public partial class User : SpacetimeDB.DatabaseTableWithPrimaryKey<User, SpacetimeDB.Types.ReducerEvent> + public partial class User : IDatabaseRow { [DataMember(Name = "identity")] - public SpacetimeDB.Identity Identity = new(); + public SpacetimeDB.Identity Identity; [DataMember(Name = "name")] public string? Name; [DataMember(Name = "online")] public bool Online; - private static Dictionary<SpacetimeDB.Identity, User> Identity_Index = new(16); - - public override void InternalOnValueInserted() - { - Identity_Index[Identity] = this; - } - - public override void InternalOnValueDeleted() - { - Identity_Index.Remove(Identity); - } - - public static User? FindByIdentity(SpacetimeDB.Identity value) - { - Identity_Index.TryGetValue(value, out var r); - return r; - } - - public static IEnumerable<User> FilterByIdentity(SpacetimeDB.Identity value) + public User( + SpacetimeDB.Identity Identity, + string? Name, + bool Online + ) { - if (FindByIdentity(value) is {} found) - { - yield return found; - } + this.Identity = Identity; + this.Name = Name; + this.Online = Online; } - public static IEnumerable<User> FilterByOnline(bool value) + public User() { - return Query(x => x.Online == value); + this.Identity = new(); } - - public override object GetPrimaryKeyValue() => Identity; - } } diff --git a/examples~/quickstart/client/module_bindings/_Globals/SpacetimeDBClient.cs b/examples~/quickstart/client/module_bindings/_Globals/SpacetimeDBClient.cs index 49dae53e..270481a2 100644 --- a/examples~/quickstart/client/module_bindings/_Globals/SpacetimeDBClient.cs +++ b/examples~/quickstart/client/module_bindings/_Globals/SpacetimeDBClient.cs @@ -5,64 +5,158 @@ #nullable enable using System; -using SpacetimeDB; +using System.Collections.Generic; + using SpacetimeDB.ClientApi; namespace SpacetimeDB.Types { - public enum ReducerType + public sealed class RemoteTables { - None, - SendMessage, - SetName, - } + public class MessageHandle : RemoteTableHandle<EventContext, Message> { + public IEnumerable<Message> FilterBySender(SpacetimeDB.Identity value) { + return Query(x => x.Sender == value); + } - public interface IReducerArgs : IReducerArgsBase - { - ReducerType ReducerType { get; } - bool InvokeHandler(ReducerEvent reducerEvent); + public IEnumerable<Message> FilterBySent(ulong value) { + return Query(x => x.Sent == value); + } + + public IEnumerable<Message> FilterByText(string value) { + return Query(x => x.Text == value); + } + } + + public class UserHandle : RemoteTableHandle<EventContext, User> { + public override object? GetPrimaryKey(IDatabaseRow row) => ((User)row).Identity; + + private Dictionary<SpacetimeDB.Identity, User> Identity_Index = new(16); + + public override void InternalInvokeValueInserted(IDatabaseRow row) { + var value = (User)row; + Identity_Index[value.Identity] = value; + } + + public override void InternalInvokeValueDeleted(IDatabaseRow row) { + Identity_Index.Remove(((User)row).Identity); + } + + public User? FindByIdentity(SpacetimeDB.Identity value) { + Identity_Index.TryGetValue(value, out var r); + return r; + } + + public IEnumerable<User> FilterByIdentity(SpacetimeDB.Identity value) { + if (FindByIdentity(value) is { } found) { + yield return found; + } + } + + public IEnumerable<User> FilterByOnline(bool value) { + return Query(x => x.Online == value); + } + } + + public readonly MessageHandle Message = new(); + public readonly UserHandle User = new(); } - public partial class ReducerEvent : ReducerEventBase + public sealed class RemoteReducers : RemoteBase<DbConnection> { - public IReducerArgs? Args { get; } + internal RemoteReducers(DbConnection conn) : base(conn) {} - public string ReducerName => Args?.ReducerName ?? "<none>"; + public delegate void SendMessageHandler(EventContext ctx, string text); + public event SendMessageHandler? OnSendMessage; - [Obsolete("ReducerType is deprecated, please match directly on type of .Args instead.")] - public ReducerType Reducer => Args?.ReducerType ?? ReducerType.None; + public void SendMessage(string text) + { + conn.InternalCallReducer(new SendMessageArgsStruct { Text = text }); + } - public ReducerEvent(IReducerArgs? args) : base() => Args = args; - public ReducerEvent(TransactionUpdate dbEvent, IReducerArgs? args) : base(dbEvent) => Args = args; + 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; - [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 void SetName(string name) + { + conn.InternalCallReducer(new SetNameArgsStruct { Name = name }); + } + + public bool InvokeSetName(EventContext ctx, SetNameArgsStruct args) + { + if (OnSetName == null) return false; + OnSetName( + ctx, + args.Name + ); + return true; + } + } + + public partial record EventContext : DbContext<RemoteTables>, IEventContext { + public readonly RemoteReducers Reducers; + public readonly Event<Reducer> Reducer; - public override bool InvokeHandler() => Args?.InvokeHandler(this) ?? false; + internal EventContext(DbConnection conn, Event<Reducer> reducer) : base(conn.RemoteTables) { + Reducers = conn.RemoteReducers; + Reducer = reducer; + } } - public class SpacetimeDBClient : SpacetimeDBClientBase<ReducerEvent> + [Type] + public partial record Reducer : TaggedEnum<( + SendMessageArgsStruct SendMessage, + SetNameArgsStruct SetName, + Unit IdentityConnected, + Unit IdentityDisconnected + )>; + + public class DbConnection : DbConnectionBase<DbConnection, Reducer> { - protected SpacetimeDBClient() + public readonly RemoteTables RemoteTables = new(); + public readonly RemoteReducers RemoteReducers; + + public DbConnection() { - clientDB.AddTable<Message>(); - clientDB.AddTable<User>(); - } + RemoteReducers = new(this); - public static readonly SpacetimeDBClient instance = new(); + clientDB.AddTable<Message>("Message", RemoteTables.Message); + clientDB.AddTable<User>("User", RemoteTables.User); + } - protected override ReducerEvent ReducerEventFromDbEvent(TransactionUpdate update) + protected override Reducer ToReducer(TransactionUpdate update) { - var argBytes = update.ReducerCall.Args; - IReducerArgs? args = update.ReducerCall.ReducerName switch { - "send_message" => BSATNHelpers.Decode<SendMessageArgsStruct>(argBytes), - "set_name" => BSATNHelpers.Decode<SetNameArgsStruct>(argBytes), - "<none>" => null, + var encodedArgs = update.ReducerCall.Args; + return update.ReducerCall.ReducerName switch { + "send_message" => new Reducer.SendMessage(BSATNHelpers.Decode<SendMessageArgsStruct>(encodedArgs)), + "set_name" => new Reducer.SetName(BSATNHelpers.Decode<SetNameArgsStruct>(encodedArgs)), + "__identity_connected__" => new Reducer.IdentityConnected(default), + "__identity_disconnected__" => new Reducer.IdentityDisconnected(default), var reducer => throw new ArgumentOutOfRangeException("Reducer", $"Unknown reducer {reducer}") }; - return new ReducerEvent(update, args); + } + + protected override IEventContext ToEventContext(Event<Reducer> reducerEvent) { + return new EventContext(this, reducerEvent); + } + + protected override bool Dispatch(IEventContext context, Reducer reducer) { + var eventContext = (EventContext)context; + return reducer switch { + Reducer.SendMessage(var args) => RemoteReducers.InvokeSendMessage(eventContext, args), + Reducer.SetName(var args) => RemoteReducers.InvokeSetName(eventContext, args), + Reducer.IdentityConnected => true, + Reducer.IdentityDisconnected => true, + _ => throw new ArgumentOutOfRangeException("Reducer", $"Unknown reducer {reducer}") + }; } } } diff --git a/examples~/quickstart/server/src/lib.rs b/examples~/quickstart/server/src/lib.rs index d47c6592..0c6572e1 100644 --- a/examples~/quickstart/server/src/lib.rs +++ b/examples~/quickstart/server/src/lib.rs @@ -1,27 +1,27 @@ -use spacetimedb::{spacetimedb, ReducerContext, Identity, Timestamp}; +use spacetimedb::{ReducerContext, Identity, Timestamp}; use anyhow::{Result, anyhow}; -#[spacetimedb(table(public))] +#[spacetimedb::table(name = User, public)] pub struct User { - #[primarykey] + #[primary_key] identity: Identity, name: Option<String>, online: bool, } -#[spacetimedb(table(public))] +#[spacetimedb::table(name = Message, public)] pub struct Message { sender: Identity, sent: Timestamp, text: String, } -#[spacetimedb(init)] +#[spacetimedb::reducer(init)] pub fn init() { // Called when the module is initially published } -#[spacetimedb(connect)] +#[spacetimedb::reducer(client_connected)] pub fn identity_connected(ctx: ReducerContext) { if let Some(user) = User::filter_by_identity(&ctx.sender) { // If this is a returning user, i.e. we already have a `User` with this `Identity`, @@ -38,7 +38,7 @@ pub fn identity_connected(ctx: ReducerContext) { } } -#[spacetimedb(disconnect)] +#[spacetimedb::reducer(client_disconnected)] pub fn identity_disconnected(ctx: ReducerContext) { if let Some(user) = User::filter_by_identity(&ctx.sender) { User::update_by_identity(&ctx.sender, User { online: false, ..user }); @@ -57,7 +57,7 @@ fn validate_name(name: String) -> Result<String> { } } -#[spacetimedb(reducer)] +#[spacetimedb::reducer] pub fn set_name(ctx: ReducerContext, name: String) -> Result<()> { let name = validate_name(name)?; if let Some(user) = User::filter_by_identity(&ctx.sender) { @@ -76,7 +76,7 @@ fn validate_message(text: String) -> Result<String> { } } -#[spacetimedb(reducer)] +#[spacetimedb::reducer] pub fn send_message(ctx: ReducerContext, text: String) -> Result<()> { // Things to consider: // - Rate-limit messages per-user. diff --git a/src/ClientCache.cs b/src/ClientCache.cs index 3b47f0fe..9992f0cb 100644 --- a/src/ClientCache.cs +++ b/src/ClientCache.cs @@ -2,27 +2,31 @@ using System.Collections; using System.Collections.Generic; using System.Linq; -using SpacetimeDB.BSATN; using SpacetimeDB.Internal; namespace SpacetimeDB { public class ClientCache { - public interface ITableCache : IEnumerable<KeyValuePair<byte[], IDatabaseTable>> + public interface ITableCache : IEnumerable<KeyValuePair<byte[], IDatabaseRow>> { Type ClientTableType { get; } - bool InsertEntry(byte[] rowBytes, IDatabaseTable value); + bool InsertEntry(byte[] rowBytes, IDatabaseRow value); bool DeleteEntry(byte[] rowBytes); - IDatabaseTable DecodeValue(byte[] bytes); + IDatabaseRow DecodeValue(byte[] bytes); + IRemoteTableHandle Handle { get; } } - public class TableCache<T> : ITableCache - where T : IDatabaseTable, IStructuralReadWrite, new() + public class TableCache<Row> : ITableCache + where Row : IDatabaseRow, new() { - public Type ClientTableType => typeof(T); + public TableCache(IRemoteTableHandle handle) => Handle = handle; - public static readonly Dictionary<byte[], T> Entries = new(ByteArrayComparer.Instance); + public IRemoteTableHandle Handle { get; init; } + + public Type ClientTableType => typeof(Row); + + public readonly Dictionary<byte[], Row> Entries = new(ByteArrayComparer.Instance); /// <summary> /// Inserts the value into the table. There can be no existing value with the provided BSATN bytes. @@ -30,7 +34,7 @@ public class TableCache<T> : ITableCache /// <param name="rowBytes">The BSATN encoded bytes of the row to retrieve.</param> /// <param name="value">The parsed row encoded by the <paramref>rowBytes</paramref>.</param> /// <returns>True if the row was inserted, false if the row wasn't inserted because it was a duplicate.</returns> - public bool InsertEntry(byte[] rowBytes, IDatabaseTable value) => Entries.TryAdd(rowBytes, (T)value); + public bool InsertEntry(byte[] rowBytes, IDatabaseRow value) => Entries.TryAdd(rowBytes, (Row)value); /// <summary> /// Deletes a value from the table. @@ -49,21 +53,21 @@ public bool DeleteEntry(byte[] rowBytes) } // The function to use for decoding a type value. - public IDatabaseTable DecodeValue(byte[] bytes) => BSATNHelpers.Decode<T>(bytes); + public IDatabaseRow DecodeValue(byte[] bytes) => BSATNHelpers.Decode<Row>(bytes); - public IEnumerator<KeyValuePair<byte[], IDatabaseTable>> GetEnumerator() => Entries.Select(kv => new KeyValuePair<byte[], IDatabaseTable>(kv.Key, kv.Value)).GetEnumerator(); + public IEnumerator<KeyValuePair<byte[], IDatabaseRow>> GetEnumerator() => Entries.Select(kv => new KeyValuePair<byte[], IDatabaseRow>(kv.Key, kv.Value)).GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } private readonly Dictionary<string, ITableCache> tables = new(); - public void AddTable<T>() - where T : IDatabaseTable, IStructuralReadWrite, new() + public void AddTable<Row>(string name, IRemoteTableHandle handle) + where Row : IDatabaseRow, new() { - string name = typeof(T).Name; - - if (!tables.TryAdd(name, new TableCache<T>())) + var cache = new TableCache<Row>(handle); + handle.SetCache(cache); + if (!tables.TryAdd(name, cache)) { Log.Error($"Table with name already exists: {name}"); } diff --git a/src/Event.cs b/src/Event.cs new file mode 100644 index 00000000..131cf3f7 --- /dev/null +++ b/src/Event.cs @@ -0,0 +1,38 @@ +using System; + +namespace SpacetimeDB +{ + public interface IEventContext { } + + public interface IReducerArgs : BSATN.IStructuralReadWrite + { + string ReducerName { get; } + } + + [Type] + public partial record Status : TaggedEnum<( + Unit Committed, + string Failed, + Unit OutOfEnergy + )>; + + public record ReducerEvent<R>( + DateTimeOffset Timestamp, + Status Status, + Identity CallerIdentity, + Address? CallerAddress, + U128? EnergyConsumed, + R Reducer + ); + + public record Event<R> + { + private Event() { } + + public record Reducer(ReducerEvent<R> ReducerEvent) : Event<R>; + public record SubscribeApplied : Event<R>; + public record UnsubscribeApplied : Event<R>; + public record SubscribeError(Exception Exception) : Event<R>; + public record UnknownTransaction : Event<R>; + } +} \ No newline at end of file diff --git a/src/IDatabaseTable.cs.meta b/src/Event.cs.meta similarity index 65% rename from src/IDatabaseTable.cs.meta rename to src/Event.cs.meta index a3227154..c1de3421 100644 --- a/src/IDatabaseTable.cs.meta +++ b/src/Event.cs.meta @@ -1,5 +1,5 @@ -fileFormatVersion: 2 -guid: 11f729b0583a241499850598f6d9c870 +fileFormatVersion: 2 +guid: 3c8f2bbd66a24c0f8710b2825daeba1d MonoImporter: externalObjects: {} serializedVersion: 2 @@ -8,4 +8,4 @@ MonoImporter: icon: {instanceID: 0} userData: assetBundleName: - assetBundleVariant: + assetBundleVariant: \ No newline at end of file diff --git a/src/IDatabaseTable.cs b/src/IDatabaseTable.cs deleted file mode 100644 index a028771a..00000000 --- a/src/IDatabaseTable.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using SpacetimeDB.BSATN; - -namespace SpacetimeDB -{ - public interface IDatabaseTable - { - void InternalOnValueInserted(); - void InternalOnValueDeleted(); - void OnInsertEvent(ReducerEventBase? update); - void OnBeforeDeleteEvent(ReducerEventBase? update); - void OnDeleteEvent(ReducerEventBase? update); - } - - public abstract class DatabaseTable<T, ReducerEvent> : IDatabaseTable - where T : DatabaseTable<T, ReducerEvent>, IStructuralReadWrite, new() - where ReducerEvent : ReducerEventBase - { - public virtual void InternalOnValueInserted() { } - - public virtual void InternalOnValueDeleted() { } - - public static IEnumerable<T> Iter() - { - return ClientCache.TableCache<T>.Entries.Values; - } - - public static IEnumerable<T> Query(Func<T, bool> filter) - { - return Iter().Where(filter); - } - - public static int Count() - { - return ClientCache.TableCache<T>.Entries.Count; - } - - public delegate void InsertEventHandler(T insertedValue, ReducerEvent? dbEvent); - public delegate void DeleteEventHandler(T deletedValue, ReducerEvent? dbEvent); - public static event InsertEventHandler? OnInsert; - public static event DeleteEventHandler? OnBeforeDelete; - public static event DeleteEventHandler? OnDelete; - - public void OnInsertEvent(ReducerEventBase? dbEvent) - { - OnInsert?.Invoke((T)this, (ReducerEvent?)dbEvent); - } - - public void OnBeforeDeleteEvent(ReducerEventBase? dbEvent) - { - OnBeforeDelete?.Invoke((T)this, (ReducerEvent?)dbEvent); - } - - public void OnDeleteEvent(ReducerEventBase? dbEvent) - { - OnDelete?.Invoke((T)this, (ReducerEvent?)dbEvent); - } - } - - public interface IDatabaseTableWithPrimaryKey : IDatabaseTable - { - void OnUpdateEvent(IDatabaseTableWithPrimaryKey newValue, ReducerEventBase? update); - object GetPrimaryKeyValue(); - } - - public abstract class DatabaseTableWithPrimaryKey<T, ReducerEvent> : DatabaseTable<T, ReducerEvent>, IDatabaseTableWithPrimaryKey - where T : DatabaseTableWithPrimaryKey<T, ReducerEvent>, IStructuralReadWrite, new() - where ReducerEvent : ReducerEventBase - { - public abstract object GetPrimaryKeyValue(); - - public delegate void UpdateEventHandler(T oldValue, T newValue, ReducerEvent? update); - public static event UpdateEventHandler? OnUpdate; - - public void OnUpdateEvent(IDatabaseTableWithPrimaryKey newValue, ReducerEventBase? dbEvent) - { - OnUpdate?.Invoke((T)this, (T)newValue, (ReducerEvent?)dbEvent); - } - } -} 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 59773506..932cf5b9 100644 --- a/src/SpacetimeDBClient.cs +++ b/src/SpacetimeDBClient.cs @@ -11,18 +11,83 @@ using SpacetimeDB.Internal; using SpacetimeDB.ClientApi; using Thread = System.Threading.Thread; +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("SpacetimeDB.Tests")] namespace SpacetimeDB { - public abstract class SpacetimeDBClientBase<ReducerEvent> - where ReducerEvent : ReducerEventBase + public sealed class DbConnectionBuilder<DbConnection, Reducer> + where DbConnection : DbConnectionBase<DbConnection, Reducer>, new() { + 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<DbConnection, Reducer> WithUri(string uri) + { + this.uri = uri; + return this; + } + + public DbConnectionBuilder<DbConnection, Reducer> WithModuleName(string nameOrAddress) + { + this.nameOrAddress = nameOrAddress; + return this; + } + + public DbConnectionBuilder<DbConnection, Reducer> WithCredentials(in (Identity identity, string token)? creds) + { + token = creds?.token; + return this; + } + + public DbConnectionBuilder<DbConnection, Reducer> OnConnect(Action<Identity, string> cb) + { + conn.onConnect += cb; + return this; + } + + public DbConnectionBuilder<DbConnection, Reducer> OnConnectError(Action<WebSocketError?, string> cb) + { + conn.webSocket.OnConnectError += (a, b) => cb.Invoke(a, b); + return this; + } + + public DbConnectionBuilder<DbConnection, Reducer> OnDisconnect(Action<DbConnection, WebSocketCloseStatus?, WebSocketError?> cb) + { + conn.webSocket.OnClose += (code, error) => cb.Invoke(conn, code, error); + return this; + } + } + + public abstract class DbConnectionBase<DbConnection, Reducer> + where DbConnection : DbConnectionBase<DbConnection, Reducer>, new() + { + public static DbConnectionBuilder<DbConnection, Reducer> Builder() => new(); + struct DbValue { - public IDatabaseTable value; + public IDatabaseRow value; public byte[] bytes; - public DbValue(IDatabaseTable value, byte[] bytes) + public DbValue(IDatabaseRow value, byte[] bytes) { this.value = value; this.bytes = bytes; @@ -36,26 +101,13 @@ struct DbOp public DbValue? insert; } - /// <summary> - /// Called when a connection is established to a spacetimedb instance. - /// </summary> - public event Action? onConnect; - - /// <summary> - /// Called when a connection attempt fails. - /// </summary> - public event Action<WebSocketError?, string>? onConnectError; + internal event Action<Identity, string>? onConnect; /// <summary> /// Called when an exception occurs when sending a message. /// </summary> public event Action<Exception>? onSendError; - /// <summary> - /// Called when a connection that was established has disconnected. - /// </summary> - public event Action<WebSocketCloseStatus?, WebSocketError?>? onDisconnect; - /// <summary> /// Invoked when a subscription is about to start being processed. This is called even before OnBeforeDelete. /// </summary> @@ -69,12 +121,7 @@ struct DbOp /// <summary> /// Invoked when a reducer is returned with an error and has no client-side handler. /// </summary> - public event Action<ReducerEvent>? onUnhandledReducerError; - - /// <summary> - /// Called when we receive an identity from the server - /// </summary> - public event Action<string, Identity, Address>? onIdentityReceived; + public event Action<ReducerEvent<Reducer>>? onUnhandledReducerError; /// <summary> /// Invoked when an event message is received or at the end of a transaction update. @@ -84,11 +131,12 @@ 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 Reducer ToReducer(TransactionUpdate update); + protected abstract IEventContext ToEventContext(Event<Reducer> reducerEvent); private readonly Dictionary<Guid, TaskCompletionSource<OneOffQueryResponse>> waitingOneOffQueries = new(); @@ -96,7 +144,7 @@ struct DbOp private readonly Thread networkMessageProcessThread; public readonly Stats stats = new(); - protected SpacetimeDBClientBase() + protected DbConnectionBase() { var options = new ConnectOptions { @@ -106,9 +154,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 +171,7 @@ struct ProcessedMessage public ServerMessage message; public List<DbOp> dbOps; public DateTime timestamp; - public ReducerEvent? reducerEvent; + public ReducerEvent<Reducer>? reducerEvent; } struct PreProcessedMessage @@ -153,6 +198,9 @@ struct PreProcessedMessage _ => throw new InvalidOperationException(), }; + private static readonly Status Committed = new Status.Committed(default); + private static readonly Status OutOfEnergy = new Status.OutOfEnergy(default); + void PreProcessMessages() { while (!isClosing) @@ -177,7 +225,7 @@ PreProcessedMessage PreProcessMessage(UnprocessedMessage unprocessed) using var binaryReader = new BinaryReader(decompressedStream); var message = new ServerMessage.BSATN().Read(binaryReader); - ReducerEvent? reducerEvent = null; + ReducerEvent<Reducer>? reducerEvent = default; // This is all of the inserts Dictionary<System.Type, HashSet<byte[]>>? subscriptionInserts = null; @@ -245,119 +293,121 @@ HashSet<byte[]> GetInsertHashSet(System.Type tableType, int tableSize) } } } - break; case ServerMessage.TransactionUpdate(var transactionUpdate): - switch (transactionUpdate.Status) + // Convert the generic event arguments in to a domain specific event object + try + { + reducerEvent = new( + DateTimeOffset.FromUnixTimeMilliseconds((long)transactionUpdate.Timestamp.Microseconds / 1000), + transactionUpdate.Status switch + { + UpdateStatus.Committed => Committed, + UpdateStatus.OutOfEnergy => OutOfEnergy, + UpdateStatus.Failed(var reason) => new Status.Failed(reason), + _ => throw new InvalidOperationException() + }, + transactionUpdate.CallerIdentity, + transactionUpdate.CallerAddress, + transactionUpdate.EnergyQuantaUsed.Quanta, + ToReducer(transactionUpdate)); + } + catch (Exception e) + { + Log.Exception(e); + } + + if (transactionUpdate.Status is UpdateStatus.Committed(var committed)) { - case UpdateStatus.Committed(var committed): - primaryKeyChanges = new(); + primaryKeyChanges = new(); - // First apply all of the state - foreach (var update in committed.Tables) + // First apply all of the state + foreach (var update in committed.Tables) + { + var tableName = update.TableName; + var table = clientDB.GetTable(tableName); + if (table == null) { - var tableName = update.TableName; - var table = clientDB.GetTable(tableName); - if (table == null) - { - Log.Error($"Unknown table name: {tableName}"); - continue; - } + Log.Error($"Unknown table name: {tableName}"); + continue; + } - foreach (var row in update.Inserts) + foreach (var row in update.Inserts) + { + var op = new DbOp { table = table, insert = Decode(table, row) }; + var pk = table.Handle.GetPrimaryKey(op.insert.Value.value); + if (pk != null) { - var op = new DbOp { table = table, insert = Decode(table, row) }; + // Compound key that we use for lookup. + // Consists of type of the table (for faster comparison that string names) + actual primary key of the row. + var key = (table.ClientTableType, pk); - if (op.insert.Value.value is IDatabaseTableWithPrimaryKey objWithPk) + if (primaryKeyChanges.TryGetValue(key, out var oldOp)) { - // Compound key that we use for lookup. - // Consists of type of the table (for faster comparison that string names) + actual primary key of the row. - var key = (table.ClientTableType, objWithPk.GetPrimaryKeyValue()); - - if (primaryKeyChanges.TryGetValue(key, out var oldOp)) + if ((op.insert is not null && oldOp.insert is not null) || (op.delete is not null && oldOp.delete is not null)) { - if ((op.insert is not null && oldOp.insert is not null) || (op.delete is not null && oldOp.delete is not null)) - { - Log.Warn($"Update with the same primary key was applied multiple times! tableName={tableName}"); - // TODO(jdetter): Is this a correctable error? This would be a major error on the - // SpacetimeDB side. - continue; - } - - var (insertOp, deleteOp) = op.insert is not null ? (op, oldOp) : (oldOp, op); - op = new DbOp - { - table = insertOp.table, - delete = deleteOp.delete, - insert = insertOp.insert, - }; + Log.Warn($"Update with the same primary key was applied multiple times! tableName={tableName}"); + // TODO(jdetter): Is this a correctable error? This would be a major error on the + // SpacetimeDB side. + continue; } - primaryKeyChanges[key] = op; - } - else - { - dbOps.Add(op); + + var (insertOp, deleteOp) = op.insert is not null ? (op, oldOp) : (oldOp, op); + op = new DbOp + { + table = insertOp.table, + delete = deleteOp.delete, + insert = insertOp.insert, + }; } + primaryKeyChanges[key] = op; + } + else + { + dbOps.Add(op); } + } - foreach (var row in update.Deletes) + foreach (var row in update.Deletes) + { + var op = new DbOp { table = table, delete = Decode(table, row) }; + var pk = table.Handle.GetPrimaryKey(op.delete.Value.value); + if (pk != null) { - var op = new DbOp { table = table, delete = Decode(table, row) }; + // Compound key that we use for lookup. + // Consists of type of the table (for faster comparison that string names) + actual primary key of the row. + var key = (table.ClientTableType, pk); - if (op.delete.Value.value is IDatabaseTableWithPrimaryKey objWithPk) + if (primaryKeyChanges.TryGetValue(key, out var oldOp)) { - // Compound key that we use for lookup. - // Consists of type of the table (for faster comparison that string names) + actual primary key of the row. - var key = (table.ClientTableType, objWithPk.GetPrimaryKeyValue()); - - if (primaryKeyChanges.TryGetValue(key, out var oldOp)) + if ((op.insert is not null && oldOp.insert is not null) || (op.delete is not null && oldOp.delete is not null)) { - if ((op.insert is not null && oldOp.insert is not null) || (op.delete is not null && oldOp.delete is not null)) - { - Log.Warn($"Update with the same primary key was applied multiple times! tableName={tableName}"); - // TODO(jdetter): Is this a correctable error? This would be a major error on the - // SpacetimeDB side. - continue; - } - - var (insertOp, deleteOp) = op.insert is not null ? (op, oldOp) : (oldOp, op); - op = new DbOp - { - table = insertOp.table, - delete = deleteOp.delete, - insert = insertOp.insert, - }; + Log.Warn($"Update with the same primary key was applied multiple times! tableName={tableName}"); + // TODO(jdetter): Is this a correctable error? This would be a major error on the + // SpacetimeDB side. + continue; } - primaryKeyChanges[key] = op; - } - else - { - dbOps.Add(op); + + var (insertOp, deleteOp) = op.insert is not null ? (op, oldOp) : (oldOp, op); + op = new DbOp + { + table = insertOp.table, + delete = deleteOp.delete, + insert = insertOp.insert, + }; } + primaryKeyChanges[key] = op; + } + else + { + dbOps.Add(op); } } + } - // Combine primary key updates and non-primary key updates - dbOps.AddRange(primaryKeyChanges.Values); - - // Convert the generic event arguments in to a domain specific event object - try - { - reducerEvent = ReducerEventFromDbEvent(transactionUpdate); - } - catch (Exception e) - { - Log.Exception(e); - } - break; - case UpdateStatus.Failed(var failed): - break; - case UpdateStatus.OutOfEnergy(var outOfEnergy): - Log.Warn("Failed to execute reducer: out of energy."); - break; - default: - throw new InvalidOperationException(); + // Combine primary key updates and non-primary key updates + dbOps.AddRange(primaryKeyChanges.Values); } break; case ServerMessage.IdentityToken(var identityToken): @@ -462,8 +512,7 @@ public void Connect(string? token, string uri, string addressOrName) }); } - - private void OnMessageProcessCompleteUpdate(ReducerEvent? dbEvent, List<DbOp> dbOps) + private void OnMessageProcessCompleteUpdate(IEventContext eventContext, List<DbOp> dbOps) { // First trigger OnBeforeDelete foreach (var update in dbOps) @@ -472,7 +521,7 @@ private void OnMessageProcessCompleteUpdate(ReducerEvent? dbEvent, List<DbOp> db { try { - oldValue.OnBeforeDeleteEvent(dbEvent!); + update.table.Handle.InvokeBeforeDelete(eventContext, oldValue); } catch (Exception e) { @@ -491,7 +540,7 @@ private void OnMessageProcessCompleteUpdate(ReducerEvent? dbEvent, List<DbOp> db { if (update.table.DeleteEntry(delete.bytes)) { - delete.value.InternalOnValueDeleted(); + update.table.Handle.InternalInvokeValueDeleted(delete.value); } else { @@ -504,7 +553,7 @@ private void OnMessageProcessCompleteUpdate(ReducerEvent? dbEvent, List<DbOp> db { if (update.table.InsertEntry(insert.bytes, insert.value)) { - insert.value.InternalOnValueInserted(); + update.table.Handle.InternalInvokeValueInserted(insert.value); } else { @@ -523,19 +572,16 @@ private void OnMessageProcessCompleteUpdate(ReducerEvent? dbEvent, List<DbOp> db { case { insert: { value: var newValue }, delete: { value: var oldValue } }: { - // If we matched an update, these values must have primary keys. - var newValue_ = (IDatabaseTableWithPrimaryKey)newValue; - var oldValue_ = (IDatabaseTableWithPrimaryKey)oldValue; - oldValue_.OnUpdateEvent(newValue_, dbEvent); + dbOp.table.Handle.InvokeUpdate(eventContext, oldValue, newValue); break; } case { insert: { value: var newValue } }: - newValue.OnInsertEvent(dbEvent); + dbOp.table.Handle.InvokeInsert(eventContext, newValue); break; case { delete: { value: var oldValue } }: - oldValue.OnDeleteEvent(dbEvent); + dbOp.table.Handle.InvokeDelete(eventContext, oldValue); break; } } @@ -546,6 +592,8 @@ private void OnMessageProcessCompleteUpdate(ReducerEvent? dbEvent, List<DbOp> db } } + protected abstract bool Dispatch(IEventContext context, Reducer reducer); + private void OnMessageProcessComplete(PreProcessedMessage preProcessed) { var processed = CalculateStateDiff(preProcessed); @@ -559,7 +607,7 @@ private void OnMessageProcessComplete(PreProcessedMessage preProcessed) onBeforeSubscriptionApplied?.Invoke(); stats.ParseMessageTracker.InsertRequest(timestamp, $"type={nameof(ServerMessage.InitialSubscription)}"); stats.SubscriptionRequestTracker.FinishTrackingRequest(initialSubscription.RequestId); - OnMessageProcessCompleteUpdate(null, dbOps); + OnMessageProcessCompleteUpdate(ToEventContext(new Event<Reducer>.SubscribeApplied()), dbOps); try { onSubscriptionApplied?.Invoke(); @@ -569,6 +617,7 @@ private void OnMessageProcessComplete(PreProcessedMessage preProcessed) Log.Exception(e); } break; + case ServerMessage.TransactionUpdate(var transactionUpdate): var reducer = transactionUpdate.ReducerCall.ReducerName; stats.ParseMessageTracker.InsertRequest(timestamp, $"type={nameof(ServerMessage.TransactionUpdate)},reducer={reducer}"); @@ -584,7 +633,15 @@ private void OnMessageProcessComplete(PreProcessedMessage preProcessed) Log.Warn($"Failed to finish tracking reducer request: {requestId}"); } } - OnMessageProcessCompleteUpdate(processed.reducerEvent, dbOps); + + if (processed.reducerEvent is not { } reducerEvent) + { + // If we are here, an error about unknown reducer should have already been logged, so nothing to do. + break; + } + + var eventContext = ToEventContext(new Event<Reducer>.Reducer(reducerEvent)); + OnMessageProcessCompleteUpdate(eventContext, dbOps); try { onEvent?.Invoke(message); @@ -594,16 +651,10 @@ private void OnMessageProcessComplete(PreProcessedMessage preProcessed) Log.Exception(e); } - if (processed.reducerEvent is not { } reducerEvent) - { - // If we are here, an error about unknown reducer should have already been logged, so nothing to do. - break; - } - var reducerFound = false; try { - reducerFound = reducerEvent.InvokeHandler(); + reducerFound = Dispatch(eventContext, reducerEvent.Reducer); } catch (Exception e) { @@ -622,18 +673,19 @@ private void OnMessageProcessComplete(PreProcessedMessage preProcessed) } } break; + case ServerMessage.IdentityToken(var identityToken): try { clientIdentity = identityToken.Identity; - var address = identityToken.Address; - onIdentityReceived?.Invoke(identityToken.Token, clientIdentity, address); + onConnect?.Invoke(identityToken.Identity, identityToken.Token); } catch (Exception e) { Log.Exception(e); } break; + case ServerMessage.OneOffQueryResponse: try { @@ -643,8 +695,8 @@ private void OnMessageProcessComplete(PreProcessedMessage preProcessed) { Log.Exception(e); } - break; + default: throw new InvalidOperationException(); } @@ -655,7 +707,7 @@ internal void OnMessageReceived(byte[] bytes, DateTime timestamp) => _messageQueue.Add(new UnprocessedMessage { bytes = bytes, timestamp = timestamp }); public void InternalCallReducer<T>(T args) - where T : IReducerArgsBase, new() + where T : IReducerArgs, new() { if (!webSocket.IsConnected) { @@ -691,7 +743,7 @@ public void Subscribe(List<string> queries) /// Usage: SpacetimeDBClientBase.instance.OneOffQuery<Message>("WHERE sender = \"bob\""); public async Task<T[]> OneOffQuery<T>(string query) - where T : IDatabaseTable, IStructuralReadWrite, new() + where T : IDatabaseRow, new() { var messageId = Guid.NewGuid(); var type = typeof(T); diff --git a/src/Stubs.cs b/src/Stubs.cs deleted file mode 100644 index f9ba6202..00000000 --- a/src/Stubs.cs +++ /dev/null @@ -1,34 +0,0 @@ -using SpacetimeDB.ClientApi; - -namespace SpacetimeDB -{ - public interface IReducerArgsBase : BSATN.IStructuralReadWrite - { - string ReducerName { get; } - } - - public abstract class ReducerEventBase - { - public ulong Timestamp { get; } - public Identity? Identity { get; } - public Address? CallerAddress { get; } - public string? ErrMessage { get; } - public UpdateStatus? Status { get; } - - public ReducerEventBase() { } - - public ReducerEventBase(TransactionUpdate update) - { - Timestamp = update.Timestamp.Microseconds; - Identity = update.CallerIdentity; - CallerAddress = update.CallerAddress; - Status = update.Status; - if (update.Status is UpdateStatus.Failed(var err)) - { - ErrMessage = err; - } - } - - public abstract bool InvokeHandler(); - } -} diff --git a/src/Table.cs b/src/Table.cs new file mode 100644 index 00000000..b2764028 --- /dev/null +++ b/src/Table.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +using SpacetimeDB.BSATN; + +namespace SpacetimeDB +{ + public interface IDatabaseRow : IStructuralReadWrite { } + + public abstract class RemoteBase<DbConnection> + { + protected readonly DbConnection conn; + + protected RemoteBase(DbConnection conn) + { + this.conn = conn; + } + } + + public interface IRemoteTableHandle + { + void SetCache(ClientCache.ITableCache cache); + + object? GetPrimaryKey(IDatabaseRow row); + + void InternalInvokeValueInserted(IDatabaseRow row); + void InternalInvokeValueDeleted(IDatabaseRow row); + void InvokeInsert(IEventContext context, IDatabaseRow row); + void InvokeDelete(IEventContext context, IDatabaseRow row); + void InvokeBeforeDelete(IEventContext context, IDatabaseRow row); + void InvokeUpdate(IEventContext context, IDatabaseRow oldRow, IDatabaseRow newRow); + } + + public abstract class RemoteTableHandle<EventContext, Row> : IRemoteTableHandle + where EventContext : class, IEventContext + where Row : IDatabaseRow, new() + { + public void SetCache(ClientCache.ITableCache cache) => Cache = (ClientCache.TableCache<Row>)cache; + + internal ClientCache.TableCache<Row>? Cache; + + public event Action<EventContext, Row>? OnInsert; + public event Action<EventContext, Row>? OnDelete; + public event Action<EventContext, Row>? OnBeforeDelete; + public event Action<EventContext, Row, Row>? OnUpdate; + + public virtual object? GetPrimaryKey(IDatabaseRow row) => null; + + public virtual void InternalInvokeValueInserted(IDatabaseRow row) { } + + public virtual void InternalInvokeValueDeleted(IDatabaseRow row) { } + + public int Count => Cache!.Entries.Count; + + public IEnumerable<Row> Iter() => Cache!.Entries.Values; + + public IEnumerable<Row> Query(Func<Row, bool> filter) => Iter().Where(filter); + + public void InvokeInsert(IEventContext context, IDatabaseRow row) => + OnInsert?.Invoke((EventContext)context, (Row)row); + + public void InvokeDelete(IEventContext context, IDatabaseRow row) => + OnDelete?.Invoke((EventContext)context, (Row)row); + + public void InvokeBeforeDelete(IEventContext context, IDatabaseRow row) => + OnBeforeDelete?.Invoke((EventContext)context, (Row)row); + + public void InvokeUpdate(IEventContext context, IDatabaseRow oldRow, IDatabaseRow newRow) => + OnUpdate?.Invoke((EventContext)context, (Row)oldRow, (Row)newRow); + } +} \ No newline at end of file diff --git a/src/Stubs.cs.meta b/src/Table.cs.meta similarity index 81% rename from src/Stubs.cs.meta rename to src/Table.cs.meta index da0198c0..09681bfa 100644 --- a/src/Stubs.cs.meta +++ b/src/Table.cs.meta @@ -1,4 +1,4 @@ -fileFormatVersion: 2 +fileFormatVersion: 2 guid: 0de7dc78e75a21a428bfa6e3dd520ba9 MonoImporter: externalObjects: {} @@ -8,4 +8,4 @@ MonoImporter: icon: {instanceID: 0} userData: assetBundleName: - assetBundleVariant: + assetBundleVariant: \ No newline at end of file diff --git a/tests~/SnapshotTests.VerifyAllTablesParsed.verified.txt b/tests~/SnapshotTests.VerifyAllTablesParsed.verified.txt index 12aa9234..fd948740 100644 --- a/tests~/SnapshotTests.VerifyAllTablesParsed.verified.txt +++ b/tests~/SnapshotTests.VerifyAllTablesParsed.verified.txt @@ -1,17 +1,33 @@ { Events: { - OnIdentityReceived: { - identity: Identity_1, - address: Guid_1 - }, + OnIdentityReceived: Identity_1, OnInsertUser: { + eventContext: { + Reducers: {Scrubbed}, + Reducer: {}, + Db: {Scrubbed} + }, user: { identity: Identity_1, online: true } }, - LogException: Unknown reducer __identity_connected__ (Parameter 'Reducer'), + LogException: Unknown reducer unknown-reducer (Parameter 'Reducer'), OnInsertUser: { + eventContext: { + Reducers: {Scrubbed}, + Reducer: { + ReducerEvent: { + Timestamp: DateTimeOffset_1, + Status: {}, + CallerIdentity: Identity_2, + CallerAddress: Guid_1, + EnergyConsumed: {}, + Reducer: {} + } + }, + Db: {Scrubbed} + }, user: { identity: Identity_2, online: true @@ -20,8 +36,8 @@ OnEvent: { status: {Scrubbed}, timestamp: 1718487763059031, - caller_identity: {Scrubbed}, - caller_address: Guid_2, + caller_identity: Identity_2, + caller_address: Guid_1, reducer_call: { reducer_name: __identity_connected__, args: @@ -30,6 +46,24 @@ host_execution_duration_micros: 66 }, OnUpdateUser: { + eventContext: { + Reducers: {Scrubbed}, + Reducer: { + ReducerEvent: { + Timestamp: DateTimeOffset_2, + Status: {}, + CallerIdentity: Identity_1, + CallerAddress: Guid_2, + EnergyConsumed: {}, + Reducer: { + SetName_: { + Name: A + } + } + } + }, + Db: {Scrubbed} + }, oldUser: { identity: Identity_1, online: true @@ -38,22 +72,13 @@ identity: Identity_1, name: A, online: true - }, - reducerEvent: { - Args: { - Name: A - }, - Timestamp: 1718487768057579, - Identity: Identity_1, - CallerAddress: Guid_1, - Status: {Scrubbed} } }, OnEvent: { status: {Scrubbed}, timestamp: 1718487768057579, - caller_identity: {Scrubbed}, - caller_address: Guid_1, + caller_identity: Identity_1, + caller_address: Guid_2, reducer_call: { reducer_name: set_name, args: AQAAAEE=, @@ -63,35 +88,53 @@ host_execution_duration_micros: 70 }, OnSetName: { - Args: { - Name: A + Reducers: {Scrubbed}, + Reducer: { + ReducerEvent: { + Timestamp: DateTimeOffset_2, + Status: {}, + CallerIdentity: Identity_1, + CallerAddress: Guid_2, + EnergyConsumed: {}, + Reducer: { + SetName_: { + Name: A + } + } + } }, - Timestamp: 1718487768057579, - Identity: Identity_1, - CallerAddress: Guid_1, - Status: {Scrubbed} + Db: {Scrubbed} }, OnInsertMessage: { + eventContext: { + Reducers: {Scrubbed}, + Reducer: { + ReducerEvent: { + Timestamp: DateTimeOffset_3, + Status: {}, + CallerIdentity: Identity_2, + CallerAddress: Guid_1, + EnergyConsumed: {}, + Reducer: { + SendMessage_: { + Text: Hello, A! + } + } + } + }, + Db: {Scrubbed} + }, message: { sender: Identity_2, sent: 1718487775346381, text: Hello, A! - }, - reducerEvent: { - Args: { - Text: Hello, A! - }, - Timestamp: 1718487775346381, - Identity: Identity_2, - CallerAddress: Guid_2, - Status: {Scrubbed} } }, OnEvent: { status: {Scrubbed}, timestamp: 1718487775346381, - caller_identity: {Scrubbed}, - caller_address: Guid_2, + caller_identity: Identity_2, + caller_address: Guid_1, reducer_call: { reducer_name: send_message, args: CQAAAEhlbGxvLCBBIQ==, @@ -101,15 +144,42 @@ host_execution_duration_micros: 57 }, OnSendMessage: { - Args: { - Text: Hello, A! + Reducers: {Scrubbed}, + Reducer: { + ReducerEvent: { + Timestamp: DateTimeOffset_3, + Status: {}, + CallerIdentity: Identity_2, + CallerAddress: Guid_1, + EnergyConsumed: {}, + Reducer: { + SendMessage_: { + Text: Hello, A! + } + } + } }, - Timestamp: 1718487775346381, - Identity: Identity_2, - CallerAddress: Guid_2, - Status: {Scrubbed} + Db: {Scrubbed} }, OnUpdateUser: { + eventContext: { + Reducers: {Scrubbed}, + Reducer: { + ReducerEvent: { + Timestamp: DateTimeOffset_4, + Status: {}, + CallerIdentity: Identity_2, + CallerAddress: Guid_1, + EnergyConsumed: {}, + Reducer: { + SetName_: { + Name: B + } + } + } + }, + Db: {Scrubbed} + }, oldUser: { identity: Identity_2, online: true @@ -118,22 +188,13 @@ identity: Identity_2, name: B, online: true - }, - reducerEvent: { - Args: { - Name: B - }, - Timestamp: 1718487777307855, - Identity: Identity_2, - CallerAddress: Guid_2, - Status: {Scrubbed} } }, OnEvent: { status: {Scrubbed}, timestamp: 1718487777307855, - caller_identity: {Scrubbed}, - caller_address: Guid_2, + caller_identity: Identity_2, + caller_address: Guid_1, reducer_call: { reducer_name: set_name, args: AQAAAEI=, @@ -143,35 +204,53 @@ host_execution_duration_micros: 98 }, OnSetName: { - Args: { - Name: B + Reducers: {Scrubbed}, + Reducer: { + ReducerEvent: { + Timestamp: DateTimeOffset_4, + Status: {}, + CallerIdentity: Identity_2, + CallerAddress: Guid_1, + EnergyConsumed: {}, + Reducer: { + SetName_: { + Name: B + } + } + } }, - Timestamp: 1718487777307855, - Identity: Identity_2, - CallerAddress: Guid_2, - Status: {Scrubbed} + Db: {Scrubbed} }, OnInsertMessage: { + eventContext: { + Reducers: {Scrubbed}, + Reducer: { + ReducerEvent: { + Timestamp: DateTimeOffset_5, + Status: {}, + CallerIdentity: Identity_1, + CallerAddress: Guid_2, + EnergyConsumed: {}, + Reducer: { + SendMessage_: { + Text: Hello, B! + } + } + } + }, + Db: {Scrubbed} + }, message: { sender: Identity_1, sent: 1718487783175083, text: Hello, B! - }, - reducerEvent: { - Args: { - Text: Hello, B! - }, - Timestamp: 1718487783175083, - Identity: Identity_1, - CallerAddress: Guid_1, - Status: {Scrubbed} } }, OnEvent: { status: {Scrubbed}, timestamp: 1718487783175083, - caller_identity: {Scrubbed}, - caller_address: Guid_1, + caller_identity: Identity_1, + caller_address: Guid_2, reducer_call: { reducer_name: send_message, args: CQAAAEhlbGxvLCBCIQ==, @@ -181,35 +260,53 @@ host_execution_duration_micros: 40 }, OnSendMessage: { - Args: { - Text: Hello, B! + Reducers: {Scrubbed}, + Reducer: { + ReducerEvent: { + Timestamp: DateTimeOffset_5, + Status: {}, + CallerIdentity: Identity_1, + CallerAddress: Guid_2, + EnergyConsumed: {}, + Reducer: { + SendMessage_: { + Text: Hello, B! + } + } + } }, - Timestamp: 1718487783175083, - Identity: Identity_1, - CallerAddress: Guid_1, - Status: {Scrubbed} + Db: {Scrubbed} }, OnInsertMessage: { + eventContext: { + Reducers: {Scrubbed}, + Reducer: { + ReducerEvent: { + Timestamp: DateTimeOffset_6, + Status: {}, + CallerIdentity: Identity_2, + CallerAddress: Guid_1, + EnergyConsumed: {}, + Reducer: { + SendMessage_: { + Text: Goodbye! + } + } + } + }, + Db: {Scrubbed} + }, message: { sender: Identity_2, sent: 1718487787645364, text: Goodbye! - }, - reducerEvent: { - Args: { - Text: Goodbye! - }, - Timestamp: 1718487787645364, - Identity: Identity_2, - CallerAddress: Guid_2, - Status: {Scrubbed} } }, OnEvent: { status: {Scrubbed}, timestamp: 1718487787645364, - caller_identity: {Scrubbed}, - caller_address: Guid_2, + caller_identity: Identity_2, + caller_address: Guid_1, reducer_call: { reducer_name: send_message, args: CAAAAEdvb2RieWUh, @@ -219,16 +316,38 @@ host_execution_duration_micros: 28 }, OnSendMessage: { - Args: { - Text: Goodbye! + Reducers: {Scrubbed}, + Reducer: { + ReducerEvent: { + Timestamp: DateTimeOffset_6, + Status: {}, + CallerIdentity: Identity_2, + CallerAddress: Guid_1, + EnergyConsumed: {}, + Reducer: { + SendMessage_: { + Text: Goodbye! + } + } + } }, - Timestamp: 1718487787645364, - Identity: Identity_2, - CallerAddress: Guid_2, - Status: {Scrubbed} + Db: {Scrubbed} }, - LogException: Unknown reducer __identity_disconnected__ (Parameter 'Reducer'), OnUpdateUser: { + eventContext: { + Reducers: {Scrubbed}, + Reducer: { + ReducerEvent: { + Timestamp: DateTimeOffset_7, + Status: {}, + CallerIdentity: Identity_2, + CallerAddress: Guid_1, + EnergyConsumed: {}, + Reducer: {} + } + }, + Db: {Scrubbed} + }, oldUser: { identity: Identity_2, name: B, @@ -243,8 +362,8 @@ OnEvent: { status: {Scrubbed}, timestamp: 1718487791901504, - caller_identity: {Scrubbed}, - caller_address: Guid_2, + caller_identity: Identity_2, + caller_address: Guid_1, reducer_call: { reducer_name: __identity_disconnected__, args: @@ -253,26 +372,35 @@ host_execution_duration_micros: 75 }, OnInsertMessage: { + eventContext: { + Reducers: {Scrubbed}, + Reducer: { + ReducerEvent: { + Timestamp: DateTimeOffset_8, + Status: {}, + CallerIdentity: Identity_1, + CallerAddress: Guid_2, + EnergyConsumed: {}, + Reducer: { + SendMessage_: { + Text: Goodbye! + } + } + } + }, + Db: {Scrubbed} + }, message: { sender: Identity_1, sent: 1718487794937841, text: Goodbye! - }, - reducerEvent: { - Args: { - Text: Goodbye! - }, - Timestamp: 1718487794937841, - Identity: Identity_1, - CallerAddress: Guid_1, - Status: {Scrubbed} } }, OnEvent: { status: {Scrubbed}, timestamp: 1718487794937841, - caller_identity: {Scrubbed}, - caller_address: Guid_1, + caller_identity: Identity_1, + caller_address: Guid_2, reducer_call: { reducer_name: send_message, args: CAAAAEdvb2RieWUh, @@ -282,13 +410,22 @@ host_execution_duration_micros: 34 }, OnSendMessage: { - Args: { - Text: Goodbye! + Reducers: {Scrubbed}, + Reducer: { + ReducerEvent: { + Timestamp: DateTimeOffset_8, + Status: {}, + CallerIdentity: Identity_1, + CallerAddress: Guid_2, + EnergyConsumed: {}, + Reducer: { + SendMessage_: { + Text: Goodbye! + } + } + } }, - Timestamp: 1718487794937841, - Identity: Identity_1, - CallerAddress: Guid_1, - Status: {Scrubbed} + Db: {Scrubbed} } }, FinalSnapshot: { @@ -330,7 +467,7 @@ Stats: { ReducerRequestTracker: { sampleCount: 3, - requestsAwaitingResponse: 5, + requestsAwaitingResponse: 6, Min: sample#4, Max: sample#2 }, @@ -341,12 +478,12 @@ Max: sample#1 }, AllReducersTracker: { - sampleCount: 8, + sampleCount: 9, Min: reducer=send_message, Max: reducer=set_name }, ParseMessageTracker: { - sampleCount: 9, + sampleCount: 10, Min: type=TransactionUpdate,reducer=send_message, Max: type=InitialSubscription } diff --git a/tests~/SnapshotTests.cs b/tests~/SnapshotTests.cs index 8aeacaa3..72486fa6 100644 --- a/tests~/SnapshotTests.cs +++ b/tests~/SnapshotTests.cs @@ -206,6 +206,9 @@ private static ServerMessage[] SampleDump() => [ SampleSubscriptionUpdate( 1, 366, [SampleUserInsert("j5DMlKmWjfbSl7qmZQOok7HDSwsAJopRSJjdlUsNogs=", null, true)] ), + SampleTransactionUpdate(0, "l0qzG1GPRtC1mwr+54q98tv0325gozLc6cNzq4vrzqY=", "Kwmeu5riP20rvCTNbBipLA==", + 0, "unknown-reducer", 0, 40, [], null + ), SampleTransactionUpdate( 1718487763059031, "l0qzG1GPRtC1mwr+54q98tv0325gozLc6cNzq4vrzqY=", "Kwmeu5riP20rvCTNbBipLA==", 0, "__identity_connected__", 1957615, 66, [SampleUserInsert("l0qzG1GPRtC1mwr+54q98tv0325gozLc6cNzq4vrzqY=", null, true)], @@ -255,7 +258,7 @@ public async Task VerifyAllTablesParsed() Log.Current = new TestLogger(events); - var client = SpacetimeDBClient.instance; + var client = new DbConnection(); var sampleDumpParsed = SampleDump(); @@ -292,35 +295,35 @@ public async Task VerifyAllTablesParsed() ServerMessage.OneOffQueryResponse(var o) => o, _ => throw new InvalidOperationException() }); - client.onIdentityReceived += (_authToken, identity, address) => - events.Add("OnIdentityReceived", new { identity, address }); + client.onConnect += (identity, _token) => + events.Add("OnIdentityReceived", identity); client.onSubscriptionApplied += () => events.Add("OnSubscriptionApplied"); client.onUnhandledReducerError += (exception) => events.Add("OnUnhandledReducerError", exception); - Reducer.OnSendMessageEvent += (reducerEvent, _text) => - events.Add("OnSendMessage", reducerEvent); - Reducer.OnSetNameEvent += (reducerEvent, _name) => events.Add("OnSetName", reducerEvent); + client.RemoteReducers.OnSendMessage += (eventContext, _text) => + events.Add("OnSendMessage", eventContext); + client.RemoteReducers.OnSetName += (eventContext, _name) => events.Add("OnSetName", eventContext); - User.OnDelete += (user, reducerEvent) => - events.Add("OnDeleteUser", new { user, reducerEvent }); - User.OnInsert += (user, reducerEvent) => - events.Add("OnInsertUser", new { user, reducerEvent }); - User.OnUpdate += (oldUser, newUser, reducerEvent) => + client.RemoteTables.User.OnDelete += (eventContext, user) => + events.Add("OnDeleteUser", new { eventContext, user }); + client.RemoteTables.User.OnInsert += (eventContext, user) => + events.Add("OnInsertUser", new { eventContext, user }); + client.RemoteTables.User.OnUpdate += (eventContext, oldUser, newUser) => events.Add( "OnUpdateUser", new { + eventContext, oldUser, - newUser, - reducerEvent + newUser } ); - Message.OnDelete += (message, reducerEvent) => - events.Add("OnDeleteMessage", new { message, reducerEvent }); - Message.OnInsert += (message, reducerEvent) => - events.Add("OnInsertMessage", new { message, reducerEvent }); + client.RemoteTables.Message.OnDelete += (eventContext, message) => + events.Add("OnDeleteMessage", new { eventContext, message }); + client.RemoteTables.Message.OnInsert += (eventContext, message) => + events.Add("OnInsertMessage", new { eventContext, message }); // Simulate receiving WebSocket messages. foreach (var sample in sampleDumpBinary) @@ -340,8 +343,8 @@ await Verify( Events = events, FinalSnapshot = new { - User = User.Iter().ToList(), - Message = Message.Iter().ToList() + User = client.RemoteTables.User.Iter().ToList(), + Message = client.RemoteTables.Message.Iter().ToList() }, Stats = client.stats } @@ -352,8 +355,8 @@ await Verify( new EnergyQuantaConverter(), new EncodedValueConverter() ])) - .ScrubMember<TransactionUpdate>(x => x.CallerIdentity) .ScrubMember<TransactionUpdate>(x => x.Status) - .ScrubMember<ReducerEventBase>(x => x.Status); + .ScrubMember<DbContext<RemoteTables>>(x => x.Db) + .ScrubMember<EventContext>(x => x.Reducers); } } diff --git a/tests~/VerifyInit.cs b/tests~/VerifyInit.cs index bc75d5e5..58d37d65 100644 --- a/tests~/VerifyInit.cs +++ b/tests~/VerifyInit.cs @@ -81,7 +81,5 @@ public static void Init() ] ) ); - - VerifierSettings.IgnoreMember<ReducerEvent>(_ => _.ReducerName); } } From f8ddab7047ae06447c5b1d7678cd929f9f452d6c Mon Sep 17 00:00:00 2001 From: Ingvar Stepanyan <me@rreverser.com> Date: Tue, 1 Oct 2024 17:17:45 +0100 Subject: [PATCH 08/55] Subscription API (#137) ## Description of Changes Implements the subscription builder (at least, the parts that are possible to implement). ## API - [ ] This is an API breaking change to the SDK *If the API is breaking, please state below what will break* ## Requires SpacetimeDB PRs *List any PRs here that are required for this SDK change to work* --- examples~/quickstart/client/Program.cs | 57 ++++--- .../_Globals/SpacetimeDBClient.cs | 2 + src/Event.cs | 60 +++++++- src/SpacetimeDBClient.cs | 143 ++++++++++-------- tests~/SnapshotTests.cs | 1 - 5 files changed, 170 insertions(+), 93 deletions(-) diff --git a/examples~/quickstart/client/Program.cs b/examples~/quickstart/client/Program.cs index 7a1fb412..cd378c59 100644 --- a/examples~/quickstart/client/Program.cs +++ b/examples~/quickstart/client/Program.cs @@ -5,30 +5,29 @@ 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 var input_queue = new ConcurrentQueue<(string Command, string Args)>(); -// declare a threadsafe cancel token to cancel the process loop -var cancel_token = new CancellationTokenSource(); void Main() { AuthToken.Init(".spacetime_csharp_quickstart"); + // TODO: just do `var conn = DbConnection...` when OnConnect signature is fixed. + DbConnection? conn = null; + conn = DbConnection.Builder() .WithUri(HOST) .WithModuleName(DBNAME) //.WithCredentials((null, AuthToken.Token)) - .OnConnect(OnConnect) + // TODO: change this to just `(OnConnect)` when signature is fixed in #131. + .OnConnect((identity, authToken) => OnConnect(conn!, identity, authToken)) .OnConnectError(OnConnectError) .OnDisconnect(OnDisconnect) .Build(); @@ -41,17 +40,19 @@ void Main() conn.RemoteReducers.OnSetName += Reducer_OnSetNameEvent; conn.RemoteReducers.OnSendMessage += Reducer_OnSendMessageEvent; - conn.onSubscriptionApplied += OnSubscriptionApplied; conn.onUnhandledReducerError += onUnhandledReducerError; + // declare a threadsafe cancel token to cancel the process loop + var cancellationTokenSource = new CancellationTokenSource(); + // spawn a thread to call process updates and process commands - var thread = new Thread(ProcessThread); + var thread = new Thread(() => ProcessThread(conn, cancellationTokenSource.Token)); thread.Start(); InputLoop(); // this signals the ProcessThread to stop - cancel_token.Cancel(); + cancellationTokenSource.Cancel(); thread.Join(); } @@ -84,9 +85,9 @@ void User_OnUpdate(EventContext ctx, User oldValue, User newValue) } } -void PrintMessage(Message message) +void PrintMessage(RemoteTables tables, Message message) { - var sender = conn.RemoteTables.User.FindByIdentity(message.Sender); + var sender = tables.User.FindByIdentity(message.Sender); var senderName = "unknown"; if (sender != null) { @@ -100,7 +101,7 @@ void Message_OnInsert(EventContext ctx, Message insertedValue) { if (ctx.Reducer is not Event<Reducer>.SubscribeApplied) { - PrintMessage(insertedValue); + PrintMessage(ctx.Db, insertedValue); } } @@ -128,12 +129,18 @@ void Reducer_OnSendMessageEvent(EventContext ctx, string text) } } -void OnConnect(Identity identity, string authToken) +void OnConnect(DbConnection conn, Identity identity, string authToken) { local_identity = identity; AuthToken.SaveToken(authToken); - conn!.Subscribe(new List<string> { "SELECT * FROM User", "SELECT * FROM Message" }); + conn.SubscriptionBuilder() + .OnApplied(OnSubscriptionApplied) + .Subscribe("SELECT * FROM User"); + + conn.SubscriptionBuilder() + .OnApplied(OnSubscriptionApplied) + .Subscribe("SELECT * FROM Message"); } void OnConnectError(WebSocketError? error, string message) @@ -146,18 +153,18 @@ void OnDisconnect(DbConnection conn, WebSocketCloseStatus? status, WebSocketErro } -void PrintMessagesInOrder() +void PrintMessagesInOrder(RemoteTables tables) { - foreach (Message message in conn.RemoteTables.Message.Iter().OrderBy(item => item.Sent)) + foreach (Message message in tables.Message.Iter().OrderBy(item => item.Sent)) { - PrintMessage(message); + PrintMessage(tables, message); } } -void OnSubscriptionApplied() +void OnSubscriptionApplied(EventContext ctx) { Console.WriteLine("Connected"); - PrintMessagesInOrder(); + PrintMessagesInOrder(ctx.Db); } void onUnhandledReducerError(ReducerEvent<Reducer> reducerEvent) @@ -165,16 +172,16 @@ void onUnhandledReducerError(ReducerEvent<Reducer> reducerEvent) Console.WriteLine($"Unhandled reducer error in {reducerEvent.Reducer}: {reducerEvent.Status}"); } -void ProcessThread() +void ProcessThread(DbConnection conn, CancellationToken ct) { try { // loop until cancellation token - while (!cancel_token.IsCancellationRequested) + while (!ct.IsCancellationRequested) { conn.Update(); - ProcessCommands(); + ProcessCommands(conn.RemoteReducers); Thread.Sleep(100); } @@ -207,7 +214,7 @@ void InputLoop() } } -void ProcessCommands() +void ProcessCommands(RemoteReducers reducers) { // process input queue commands while (input_queue.TryDequeue(out var command)) @@ -215,10 +222,10 @@ void ProcessCommands() switch (command.Command) { case "message": - conn.RemoteReducers.SendMessage(command.Args); + reducers.SendMessage(command.Args); break; case "name": - conn.RemoteReducers.SetName(command.Args); + reducers.SetName(command.Args); break; } } diff --git a/examples~/quickstart/client/module_bindings/_Globals/SpacetimeDBClient.cs b/examples~/quickstart/client/module_bindings/_Globals/SpacetimeDBClient.cs index 270481a2..cf4f4e95 100644 --- a/examples~/quickstart/client/module_bindings/_Globals/SpacetimeDBClient.cs +++ b/examples~/quickstart/client/module_bindings/_Globals/SpacetimeDBClient.cs @@ -158,5 +158,7 @@ protected override bool Dispatch(IEventContext context, Reducer reducer) { _ => throw new ArgumentOutOfRangeException("Reducer", $"Unknown reducer {reducer}") }; } + + public SubscriptionBuilder<EventContext> SubscriptionBuilder() => new(this); } } diff --git a/src/Event.cs b/src/Event.cs index 131cf3f7..ff687c0f 100644 --- a/src/Event.cs +++ b/src/Event.cs @@ -35,4 +35,62 @@ public record UnsubscribeApplied : Event<R>; public record SubscribeError(Exception Exception) : Event<R>; public record UnknownTransaction : Event<R>; } -} \ No newline at end of file + + // TODO: Move those classes into EventContext, so that we wouldn't need repetitive generics. + public sealed class SubscriptionBuilder<EventContext> + where EventContext : IEventContext + { + private readonly IDbConnection conn; + private event Action<EventContext>? Applied; + private event Action<EventContext>? Error; + + public SubscriptionBuilder(IDbConnection conn) + { + this.conn = conn; + } + + public SubscriptionBuilder<EventContext> OnApplied(Action<EventContext> callback) + { + Applied += callback; + return this; + } + + public SubscriptionBuilder<EventContext> OnError(Action<EventContext> callback) + { + Error += callback; + return this; + } + + public SubscriptionHandle<EventContext> Subscribe(string querySql) => new(conn, Applied, Error, querySql); + } + + public interface ISubscriptionHandle + { + void OnApplied(IEventContext ctx); + } + + public class SubscriptionHandle<EventContext> : ISubscriptionHandle + where EventContext : IEventContext + { + private readonly Action<EventContext>? onApplied; + + void ISubscriptionHandle.OnApplied(IEventContext ctx) + { + IsActive = true; + onApplied?.Invoke((EventContext)ctx); + } + + internal SubscriptionHandle(IDbConnection conn, Action<EventContext>? onApplied, Action<EventContext>? onError, string querySql) + { + this.onApplied = onApplied; + conn.Subscribe(this, querySql); + } + + public void Unsubscribe() => throw new NotImplementedException(); + + public void UnsuscribeThen(Action<EventContext> onEnd) => throw new NotImplementedException(); + + public bool IsEnded => false; + public bool IsActive { get; private set; } + } +} diff --git a/src/SpacetimeDBClient.cs b/src/SpacetimeDBClient.cs index 932cf5b9..ca18cd84 100644 --- a/src/SpacetimeDBClient.cs +++ b/src/SpacetimeDBClient.cs @@ -77,7 +77,12 @@ public DbConnectionBuilder<DbConnection, Reducer> OnDisconnect(Action<DbConnecti } } - public abstract class DbConnectionBase<DbConnection, Reducer> + public interface IDbConnection + { + void Subscribe(ISubscriptionHandle handle, string query); + } + + public abstract class DbConnectionBase<DbConnection, Reducer> : IDbConnection where DbConnection : DbConnectionBase<DbConnection, Reducer>, new() { public static DbConnectionBuilder<DbConnection, Reducer> Builder() => new(); @@ -108,16 +113,13 @@ struct DbOp /// </summary> public event Action<Exception>? onSendError; + private readonly Dictionary<uint, ISubscriptionHandle> subscriptions = new(); + /// <summary> /// Invoked when a subscription is about to start being processed. This is called even before OnBeforeDelete. /// </summary> public event Action? onBeforeSubscriptionApplied; - /// <summary> - /// Invoked when the local client cache is updated as a result of changes made to the subscription queries. - /// </summary> - public event Action? onSubscriptionApplied; - /// <summary> /// Invoked when a reducer is returned with an error and has no client-side handler. /// </summary> @@ -604,76 +606,82 @@ private void OnMessageProcessComplete(PreProcessedMessage preProcessed) switch (message) { case ServerMessage.InitialSubscription(var initialSubscription): - onBeforeSubscriptionApplied?.Invoke(); - stats.ParseMessageTracker.InsertRequest(timestamp, $"type={nameof(ServerMessage.InitialSubscription)}"); - stats.SubscriptionRequestTracker.FinishTrackingRequest(initialSubscription.RequestId); - OnMessageProcessCompleteUpdate(ToEventContext(new Event<Reducer>.SubscribeApplied()), dbOps); - try - { - onSubscriptionApplied?.Invoke(); - } - catch (Exception e) { - Log.Exception(e); + onBeforeSubscriptionApplied?.Invoke(); + stats.ParseMessageTracker.InsertRequest(timestamp, $"type={nameof(ServerMessage.InitialSubscription)}"); + stats.SubscriptionRequestTracker.FinishTrackingRequest(initialSubscription.RequestId); + var eventContext = ToEventContext(new Event<Reducer>.SubscribeApplied()); + OnMessageProcessCompleteUpdate(eventContext, dbOps); + if (subscriptions.TryGetValue(initialSubscription.RequestId, out var subscription)) + { + try + { + subscription.OnApplied(eventContext); + } + catch (Exception e) + { + Log.Exception(e); + } + } + break; } - break; - case ServerMessage.TransactionUpdate(var transactionUpdate): - var reducer = transactionUpdate.ReducerCall.ReducerName; - stats.ParseMessageTracker.InsertRequest(timestamp, $"type={nameof(ServerMessage.TransactionUpdate)},reducer={reducer}"); - var hostDuration = TimeSpan.FromMilliseconds(transactionUpdate.HostExecutionDurationMicros / 1000.0d); - stats.AllReducersTracker.InsertRequest(hostDuration, $"reducer={reducer}"); - var callerIdentity = transactionUpdate.CallerIdentity; - if (callerIdentity == clientIdentity) { - // This was a request that we initiated - var requestId = transactionUpdate.ReducerCall.RequestId; - if (!stats.ReducerRequestTracker.FinishTrackingRequest(requestId)) + var reducer = transactionUpdate.ReducerCall.ReducerName; + stats.ParseMessageTracker.InsertRequest(timestamp, $"type={nameof(ServerMessage.TransactionUpdate)},reducer={reducer}"); + var hostDuration = TimeSpan.FromMilliseconds(transactionUpdate.HostExecutionDurationMicros / 1000.0d); + stats.AllReducersTracker.InsertRequest(hostDuration, $"reducer={reducer}"); + var callerIdentity = transactionUpdate.CallerIdentity; + if (callerIdentity == clientIdentity) { - Log.Warn($"Failed to finish tracking reducer request: {requestId}"); + // This was a request that we initiated + var requestId = transactionUpdate.ReducerCall.RequestId; + if (!stats.ReducerRequestTracker.FinishTrackingRequest(requestId)) + { + Log.Warn($"Failed to finish tracking reducer request: {requestId}"); + } } - } - if (processed.reducerEvent is not { } reducerEvent) - { - // If we are here, an error about unknown reducer should have already been logged, so nothing to do. - break; - } - - var eventContext = ToEventContext(new Event<Reducer>.Reducer(reducerEvent)); - OnMessageProcessCompleteUpdate(eventContext, dbOps); - try - { - onEvent?.Invoke(message); - } - catch (Exception e) - { - Log.Exception(e); - } + if (processed.reducerEvent is not { } reducerEvent) + { + // If we are here, an error about unknown reducer should have already been logged, so nothing to do. + break; + } - var reducerFound = false; - try - { - reducerFound = Dispatch(eventContext, reducerEvent.Reducer); - } - catch (Exception e) - { - Log.Exception(e); - } + var eventContext = ToEventContext(new Event<Reducer>.Reducer(reducerEvent)); + OnMessageProcessCompleteUpdate(eventContext, dbOps); + try + { + onEvent?.Invoke(message); + } + catch (Exception e) + { + Log.Exception(e); + } - if (!reducerFound && transactionUpdate.Status is UpdateStatus.Failed(var failed)) - { + var reducerFound = false; try { - onUnhandledReducerError?.Invoke(reducerEvent); + reducerFound = Dispatch(eventContext, reducerEvent.Reducer); } catch (Exception e) { Log.Exception(e); } - } - break; + if (!reducerFound && transactionUpdate.Status is UpdateStatus.Failed(var failed)) + { + try + { + onUnhandledReducerError?.Invoke(reducerEvent); + } + catch (Exception e) + { + Log.Exception(e); + } + } + break; + } case ServerMessage.IdentityToken(var identityToken): try { @@ -725,7 +733,7 @@ public void InternalCallReducer<T>(T args) )); } - public void Subscribe(List<string> queries) + void IDbConnection.Subscribe(ISubscriptionHandle handle, string query) { if (!webSocket.IsConnected) { @@ -733,12 +741,15 @@ public void Subscribe(List<string> queries) return; } - var request = new Subscribe - { - RequestId = stats.SubscriptionRequestTracker.StartTrackingRequest(), - }; - request.QueryStrings.AddRange(queries); - webSocket.Send(new ClientMessage.Subscribe(request)); + var id = stats.SubscriptionRequestTracker.StartTrackingRequest(); + subscriptions[id] = handle; + webSocket.Send(new ClientMessage.Subscribe( + new Subscribe + { + RequestId = id, + QueryStrings = { query } + } + )); } /// Usage: SpacetimeDBClientBase.instance.OneOffQuery<Message>("WHERE sender = \"bob\""); diff --git a/tests~/SnapshotTests.cs b/tests~/SnapshotTests.cs index 72486fa6..e99e601c 100644 --- a/tests~/SnapshotTests.cs +++ b/tests~/SnapshotTests.cs @@ -297,7 +297,6 @@ public async Task VerifyAllTablesParsed() }); client.onConnect += (identity, _token) => events.Add("OnIdentityReceived", identity); - client.onSubscriptionApplied += () => events.Add("OnSubscriptionApplied"); client.onUnhandledReducerError += (exception) => events.Add("OnUnhandledReducerError", exception); From 0981b8917e4b9f3ed59c8b608c5a17dc76d589f5 Mon Sep 17 00:00:00 2001 From: Ingvar Stepanyan <me@rreverser.com> Date: Tue, 1 Oct 2024 17:37:42 +0100 Subject: [PATCH 09/55] Merge table cache into table handle (#139) ## Description of Changes Merges cache into the table handle as suggested on the original PR + hides most table methods that shouldn't be part of the stable API. Few remaining methods will need a codegen change to be available only to subclasses, so for now that's out of scope. Same for merging ClientCache into RemoteTables - we shouldn't need a separate collection, and instead could autogenerate a switch expression over table name. ## API - [ ] This is an API breaking change to the SDK *If the API is breaking, please state below what will break* ## Requires SpacetimeDB PRs *List any PRs here that are required for this SDK change to work* --- src/ClientCache.cs | 66 ++++---------------------------- src/SpacetimeDBClient.cs | 44 ++++++++++----------- src/Table.cs | 82 +++++++++++++++++++++++++++++----------- 3 files changed, 87 insertions(+), 105 deletions(-) diff --git a/src/ClientCache.cs b/src/ClientCache.cs index 9992f0cb..824ca34b 100644 --- a/src/ClientCache.cs +++ b/src/ClientCache.cs @@ -6,74 +6,22 @@ namespace SpacetimeDB { + // TODO: merge this into `RemoteTables`. + // It should just provide auto-generated `GetTable` and `GetTables` methods. public class ClientCache { - public interface ITableCache : IEnumerable<KeyValuePair<byte[], IDatabaseRow>> - { - Type ClientTableType { get; } - bool InsertEntry(byte[] rowBytes, IDatabaseRow value); - bool DeleteEntry(byte[] rowBytes); - IDatabaseRow DecodeValue(byte[] bytes); - IRemoteTableHandle Handle { get; } - } - - public class TableCache<Row> : ITableCache - where Row : IDatabaseRow, new() - { - public TableCache(IRemoteTableHandle handle) => Handle = handle; - - public IRemoteTableHandle Handle { get; init; } - - public Type ClientTableType => typeof(Row); - - public readonly Dictionary<byte[], Row> Entries = new(ByteArrayComparer.Instance); - - /// <summary> - /// Inserts the value into the table. There can be no existing value with the provided BSATN bytes. - /// </summary> - /// <param name="rowBytes">The BSATN encoded bytes of the row to retrieve.</param> - /// <param name="value">The parsed row encoded by the <paramref>rowBytes</paramref>.</param> - /// <returns>True if the row was inserted, false if the row wasn't inserted because it was a duplicate.</returns> - public bool InsertEntry(byte[] rowBytes, IDatabaseRow value) => Entries.TryAdd(rowBytes, (Row)value); - - /// <summary> - /// Deletes a value from the table. - /// </summary> - /// <param name="rowBytes">The BSATN encoded bytes of the row to remove.</param> - /// <returns>True if and only if the value was previously resident and has been deleted.</returns> - public bool DeleteEntry(byte[] rowBytes) - { - if (Entries.Remove(rowBytes)) - { - return true; - } - - Log.Warn("Deleting value that we don't have (no cached value available)"); - return false; - } - - // The function to use for decoding a type value. - public IDatabaseRow DecodeValue(byte[] bytes) => BSATNHelpers.Decode<Row>(bytes); - - public IEnumerator<KeyValuePair<byte[], IDatabaseRow>> GetEnumerator() => Entries.Select(kv => new KeyValuePair<byte[], IDatabaseRow>(kv.Key, kv.Value)).GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - } - - private readonly Dictionary<string, ITableCache> tables = new(); + private readonly Dictionary<string, IRemoteTableHandle> tables = new(); - public void AddTable<Row>(string name, IRemoteTableHandle handle) + public void AddTable<Row>(string name, IRemoteTableHandle table) where Row : IDatabaseRow, new() { - var cache = new TableCache<Row>(handle); - handle.SetCache(cache); - if (!tables.TryAdd(name, cache)) + if (!tables.TryAdd(name, table)) { Log.Error($"Table with name already exists: {name}"); } } - public ITableCache? GetTable(string name) + public IRemoteTableHandle? GetTable(string name) { if (tables.TryGetValue(name, out var table)) { @@ -84,6 +32,6 @@ public void AddTable<Row>(string name, IRemoteTableHandle handle) return null; } - public IEnumerable<ITableCache> GetTables() => tables.Values; + public IEnumerable<IRemoteTableHandle> GetTables() => tables.Values; } } diff --git a/src/SpacetimeDBClient.cs b/src/SpacetimeDBClient.cs index ca18cd84..fcdcdce8 100644 --- a/src/SpacetimeDBClient.cs +++ b/src/SpacetimeDBClient.cs @@ -87,10 +87,10 @@ public abstract class DbConnectionBase<DbConnection, Reducer> : IDbConnection { public static DbConnectionBuilder<DbConnection, Reducer> Builder() => new(); - struct DbValue + readonly struct DbValue { - public IDatabaseRow value; - public byte[] bytes; + public readonly IDatabaseRow value; + public readonly byte[] bytes; public DbValue(IDatabaseRow value, byte[] bytes) { @@ -101,7 +101,7 @@ public DbValue(IDatabaseRow value, byte[] bytes) struct DbOp { - public ClientCache.ITableCache table; + public IRemoteTableHandle table; public DbValue? delete; public DbValue? insert; } @@ -193,12 +193,14 @@ struct PreProcessedMessage private readonly CancellationTokenSource _preProcessCancellationTokenSource = new(); private CancellationToken _preProcessCancellationToken => _preProcessCancellationTokenSource.Token; - static DbValue Decode(ClientCache.ITableCache table, EncodedValue value) => value switch + static DbValue Decode(IRemoteTableHandle table, EncodedValue value, out object? primaryKey) { - EncodedValue.Binary(var bin) => new DbValue(table.DecodeValue(bin), bin), - EncodedValue.Text(var text) => throw new InvalidOperationException("JavaScript messages aren't supported."), - _ => throw new InvalidOperationException(), - }; + // We expect only binary messages here; let type cast exception take care of any others. + var bin = ((EncodedValue.Binary)value).Binary_; + var obj = table.DecodeValue(bin); + primaryKey = table.GetPrimaryKey(obj); + return new(obj, bin); + } private static readonly Status Committed = new Status.Committed(default); private static readonly Status OutOfEnergy = new Status.OutOfEnergy(default); @@ -337,8 +339,7 @@ HashSet<byte[]> GetInsertHashSet(System.Type tableType, int tableSize) foreach (var row in update.Inserts) { - var op = new DbOp { table = table, insert = Decode(table, row) }; - var pk = table.Handle.GetPrimaryKey(op.insert.Value.value); + var op = new DbOp { table = table, insert = Decode(table, row, out var pk) }; if (pk != null) { // Compound key that we use for lookup. @@ -373,8 +374,7 @@ HashSet<byte[]> GetInsertHashSet(System.Type tableType, int tableSize) foreach (var row in update.Deletes) { - var op = new DbOp { table = table, delete = Decode(table, row) }; - var pk = table.Handle.GetPrimaryKey(op.delete.Value.value); + var op = new DbOp { table = table, delete = Decode(table, row, out var pk) }; if (pk != null) { // Compound key that we use for lookup. @@ -454,7 +454,7 @@ ProcessedMessage CalculateStateDiff(PreProcessedMessage preProcessedMessage) continue; } - foreach (var (rowBytes, oldValue) in table.Where(kv => !hashSet.Contains(kv.Key))) + foreach (var (rowBytes, oldValue) in table.IterEntries().Where(kv => !hashSet.Contains(kv.Key))) { processed.dbOps.Add(new DbOp { @@ -523,7 +523,7 @@ private void OnMessageProcessCompleteUpdate(IEventContext eventContext, List<DbO { try { - update.table.Handle.InvokeBeforeDelete(eventContext, oldValue); + update.table.InvokeBeforeDelete(eventContext, oldValue); } catch (Exception e) { @@ -542,7 +542,7 @@ private void OnMessageProcessCompleteUpdate(IEventContext eventContext, List<DbO { if (update.table.DeleteEntry(delete.bytes)) { - update.table.Handle.InternalInvokeValueDeleted(delete.value); + update.table.InternalInvokeValueDeleted(delete.value); } else { @@ -555,7 +555,7 @@ private void OnMessageProcessCompleteUpdate(IEventContext eventContext, List<DbO { if (update.table.InsertEntry(insert.bytes, insert.value)) { - update.table.Handle.InternalInvokeValueInserted(insert.value); + update.table.InternalInvokeValueInserted(insert.value); } else { @@ -573,17 +573,15 @@ private void OnMessageProcessCompleteUpdate(IEventContext eventContext, List<DbO switch (dbOp) { case { insert: { value: var newValue }, delete: { value: var oldValue } }: - { - dbOp.table.Handle.InvokeUpdate(eventContext, oldValue, newValue); - break; - } + dbOp.table.InvokeUpdate(eventContext, oldValue, newValue); + break; case { insert: { value: var newValue } }: - dbOp.table.Handle.InvokeInsert(eventContext, newValue); + dbOp.table.InvokeInsert(eventContext, newValue); break; case { delete: { value: var oldValue } }: - dbOp.table.Handle.InvokeDelete(eventContext, oldValue); + dbOp.table.InvokeDelete(eventContext, oldValue); break; } } diff --git a/src/Table.cs b/src/Table.cs index b2764028..2f5f3c6e 100644 --- a/src/Table.cs +++ b/src/Table.cs @@ -20,53 +20,89 @@ protected RemoteBase(DbConnection conn) public interface IRemoteTableHandle { - void SetCache(ClientCache.ITableCache cache); - + // These methods need to be overridden by autogen. object? GetPrimaryKey(IDatabaseRow row); - void InternalInvokeValueInserted(IDatabaseRow row); void InternalInvokeValueDeleted(IDatabaseRow row); - void InvokeInsert(IEventContext context, IDatabaseRow row); - void InvokeDelete(IEventContext context, IDatabaseRow row); - void InvokeBeforeDelete(IEventContext context, IDatabaseRow row); - void InvokeUpdate(IEventContext context, IDatabaseRow oldRow, IDatabaseRow newRow); + + // These are provided by RemoteTableHandle. + internal Type ClientTableType { get; } + internal IEnumerable<KeyValuePair<byte[], IDatabaseRow>> IterEntries(); + internal bool InsertEntry(byte[] rowBytes, IDatabaseRow value); + internal bool DeleteEntry(byte[] rowBytes); + internal IDatabaseRow DecodeValue(byte[] bytes); + + internal void InvokeInsert(IEventContext context, IDatabaseRow row); + internal void InvokeDelete(IEventContext context, IDatabaseRow row); + internal void InvokeBeforeDelete(IEventContext context, IDatabaseRow row); + internal void InvokeUpdate(IEventContext context, IDatabaseRow oldRow, IDatabaseRow newRow); } public abstract class RemoteTableHandle<EventContext, Row> : IRemoteTableHandle where EventContext : class, IEventContext where Row : IDatabaseRow, new() { - public void SetCache(ClientCache.ITableCache cache) => Cache = (ClientCache.TableCache<Row>)cache; + // These methods need to be overridden by autogen. + public virtual object? GetPrimaryKey(IDatabaseRow row) => null; + public virtual void InternalInvokeValueInserted(IDatabaseRow row) { } + public virtual void InternalInvokeValueDeleted(IDatabaseRow row) { } + + // These are provided by RemoteTableHandle. + Type IRemoteTableHandle.ClientTableType => typeof(Row); + + private readonly Dictionary<byte[], Row> Entries = new(Internal.ByteArrayComparer.Instance); + + IEnumerable<KeyValuePair<byte[], IDatabaseRow>> IRemoteTableHandle.IterEntries() => + Entries.Select(kv => new KeyValuePair<byte[], IDatabaseRow>(kv.Key, kv.Value)); + + /// <summary> + /// Inserts the value into the table. There can be no existing value with the provided BSATN bytes. + /// </summary> + /// <param name="rowBytes">The BSATN encoded bytes of the row to retrieve.</param> + /// <param name="value">The parsed row encoded by the <paramref>rowBytes</paramref>.</param> + /// <returns>True if the row was inserted, false if the row wasn't inserted because it was a duplicate.</returns> + bool IRemoteTableHandle.InsertEntry(byte[] rowBytes, IDatabaseRow value) => Entries.TryAdd(rowBytes, (Row)value); - internal ClientCache.TableCache<Row>? Cache; + /// <summary> + /// Deletes a value from the table. + /// </summary> + /// <param name="rowBytes">The BSATN encoded bytes of the row to remove.</param> + /// <returns>True if and only if the value was previously resident and has been deleted.</returns> + bool IRemoteTableHandle.DeleteEntry(byte[] rowBytes) + { + if (Entries.Remove(rowBytes)) + { + return true; + } + + Log.Warn("Deleting value that we don't have (no cached value available)"); + return false; + } + + // The function to use for decoding a type value. + IDatabaseRow IRemoteTableHandle.DecodeValue(byte[] bytes) => BSATNHelpers.Decode<Row>(bytes); public event Action<EventContext, Row>? OnInsert; public event Action<EventContext, Row>? OnDelete; public event Action<EventContext, Row>? OnBeforeDelete; public event Action<EventContext, Row, Row>? OnUpdate; - public virtual object? GetPrimaryKey(IDatabaseRow row) => null; - - public virtual void InternalInvokeValueInserted(IDatabaseRow row) { } - - public virtual void InternalInvokeValueDeleted(IDatabaseRow row) { } - - public int Count => Cache!.Entries.Count; + public int Count => Entries.Count; - public IEnumerable<Row> Iter() => Cache!.Entries.Values; + public IEnumerable<Row> Iter() => Entries.Values; - public IEnumerable<Row> Query(Func<Row, bool> filter) => Iter().Where(filter); + protected IEnumerable<Row> Query(Func<Row, bool> filter) => Iter().Where(filter); - public void InvokeInsert(IEventContext context, IDatabaseRow row) => + void IRemoteTableHandle.InvokeInsert(IEventContext context, IDatabaseRow row) => OnInsert?.Invoke((EventContext)context, (Row)row); - public void InvokeDelete(IEventContext context, IDatabaseRow row) => + void IRemoteTableHandle.InvokeDelete(IEventContext context, IDatabaseRow row) => OnDelete?.Invoke((EventContext)context, (Row)row); - public void InvokeBeforeDelete(IEventContext context, IDatabaseRow row) => + void IRemoteTableHandle.InvokeBeforeDelete(IEventContext context, IDatabaseRow row) => OnBeforeDelete?.Invoke((EventContext)context, (Row)row); - public void InvokeUpdate(IEventContext context, IDatabaseRow oldRow, IDatabaseRow newRow) => + void IRemoteTableHandle.InvokeUpdate(IEventContext context, IDatabaseRow oldRow, IDatabaseRow newRow) => OnUpdate?.Invoke((EventContext)context, (Row)oldRow, (Row)newRow); } -} \ No newline at end of file +} From 2aae961ec1448551b1be40ffdcef3197f6c6f16c Mon Sep 17 00:00:00 2001 From: Jeremie Pelletier <jeremiep@gmail.com> Date: Tue, 1 Oct 2024 13:02:54 -0400 Subject: [PATCH 10/55] Add DbConnection argument to OnConnect (#138) ## Description of Changes *Describe what has been changed, any new features or bug fixes* ## API - [ ] This is an API breaking change to the SDK *If the API is breaking, please state below what will break* ## Requires SpacetimeDB PRs *List any PRs here that are required for this SDK change to work* --- examples~/quickstart/client/Program.cs | 3 +-- src/SpacetimeDBClient.cs | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/examples~/quickstart/client/Program.cs b/examples~/quickstart/client/Program.cs index cd378c59..0f62f3d4 100644 --- a/examples~/quickstart/client/Program.cs +++ b/examples~/quickstart/client/Program.cs @@ -26,8 +26,7 @@ void Main() .WithUri(HOST) .WithModuleName(DBNAME) //.WithCredentials((null, AuthToken.Token)) - // TODO: change this to just `(OnConnect)` when signature is fixed in #131. - .OnConnect((identity, authToken) => OnConnect(conn!, identity, authToken)) + .OnConnect(OnConnect) .OnConnectError(OnConnectError) .OnDisconnect(OnDisconnect) .Build(); diff --git a/src/SpacetimeDBClient.cs b/src/SpacetimeDBClient.cs index fcdcdce8..b7c405f8 100644 --- a/src/SpacetimeDBClient.cs +++ b/src/SpacetimeDBClient.cs @@ -58,9 +58,9 @@ public DbConnectionBuilder<DbConnection, Reducer> WithCredentials(in (Identity i return this; } - public DbConnectionBuilder<DbConnection, Reducer> OnConnect(Action<Identity, string> cb) + public DbConnectionBuilder<DbConnection, Reducer> OnConnect(Action<DbConnection, Identity, string> cb) { - conn.onConnect += cb; + conn.onConnect += (identity, token) => cb.Invoke(conn, identity, token); return this; } From 95b9d1756ccb6667a52b63b8fede1d884c176f49 Mon Sep 17 00:00:00 2001 From: Ingvar Stepanyan <me@rreverser.com> Date: Tue, 1 Oct 2024 20:00:32 +0100 Subject: [PATCH 11/55] Hide more APIs (#140) ## Description of Changes Removing unstable APIs that are not used by BitCraft; marking others with [Obsolete] and renaming few others to match the proposal. One exception is InternalCallReducer - it would need some further changes to codegen; marking it as Obsolete right now would cause all generated clients to show noisy warnings. ## API - [x] This is an API breaking change to the SDK *If the API is breaking, please state below what will break* ## Requires SpacetimeDB PRs *List any PRs here that are required for this SDK change to work* --- examples~/quickstart/client/Program.cs | 6 +- src/ClientCache.cs | 4 +- src/ConsoleLogger.cs | 2 +- src/SpacetimeDBClient.cs | 50 +++----- src/UnityDebugLogger.cs | 2 +- src/WebSocket.cs | 33 +++--- ...otTests.VerifyAllTablesParsed.verified.txt | 108 +----------------- tests~/SnapshotTests.cs | 23 ++-- tests~/tests.csproj | 2 +- 9 files changed, 52 insertions(+), 178 deletions(-) diff --git a/examples~/quickstart/client/Program.cs b/examples~/quickstart/client/Program.cs index 0f62f3d4..29ad6c95 100644 --- a/examples~/quickstart/client/Program.cs +++ b/examples~/quickstart/client/Program.cs @@ -39,7 +39,9 @@ void Main() conn.RemoteReducers.OnSetName += Reducer_OnSetNameEvent; conn.RemoteReducers.OnSendMessage += Reducer_OnSendMessageEvent; +#pragma warning disable CS0612 // Using obsolete API conn.onUnhandledReducerError += onUnhandledReducerError; +#pragma warning restore CS0612 // Using obsolete API // declare a threadsafe cancel token to cancel the process loop var cancellationTokenSource = new CancellationTokenSource(); @@ -178,7 +180,7 @@ void ProcessThread(DbConnection conn, CancellationToken ct) // loop until cancellation token while (!ct.IsCancellationRequested) { - conn.Update(); + conn.FrameTick(); ProcessCommands(conn.RemoteReducers); @@ -187,7 +189,7 @@ void ProcessThread(DbConnection conn, CancellationToken ct) } finally { - conn.Close(); + conn.Disconnect(); } } diff --git a/src/ClientCache.cs b/src/ClientCache.cs index 824ca34b..0ec8ad91 100644 --- a/src/ClientCache.cs +++ b/src/ClientCache.cs @@ -21,7 +21,7 @@ public void AddTable<Row>(string name, IRemoteTableHandle table) } } - public IRemoteTableHandle? GetTable(string name) + internal IRemoteTableHandle? GetTable(string name) { if (tables.TryGetValue(name, out var table)) { @@ -32,6 +32,6 @@ public void AddTable<Row>(string name, IRemoteTableHandle table) return null; } - public IEnumerable<IRemoteTableHandle> GetTables() => tables.Values; + internal IEnumerable<IRemoteTableHandle> GetTables() => tables.Values; } } diff --git a/src/ConsoleLogger.cs b/src/ConsoleLogger.cs index 1711899c..2fda0ab5 100644 --- a/src/ConsoleLogger.cs +++ b/src/ConsoleLogger.cs @@ -2,7 +2,7 @@ namespace SpacetimeDB { - public class ConsoleLogger : ISpacetimeDBLogger + internal class ConsoleLogger : ISpacetimeDBLogger { [Flags] public enum LogLevel diff --git a/src/SpacetimeDBClient.cs b/src/SpacetimeDBClient.cs index b7c405f8..88c85ccc 100644 --- a/src/SpacetimeDBClient.cs +++ b/src/SpacetimeDBClient.cs @@ -111,27 +111,19 @@ struct DbOp /// <summary> /// Called when an exception occurs when sending a message. /// </summary> + [Obsolete] public event Action<Exception>? onSendError; private readonly Dictionary<uint, ISubscriptionHandle> subscriptions = new(); - /// <summary> - /// Invoked when a subscription is about to start being processed. This is called even before OnBeforeDelete. - /// </summary> - public event Action? onBeforeSubscriptionApplied; - /// <summary> /// Invoked when a reducer is returned with an error and has no client-side handler. /// </summary> + [Obsolete] public event Action<ReducerEvent<Reducer>>? onUnhandledReducerError; - /// <summary> - /// Invoked when an event message is received or at the end of a transaction update. - /// </summary> - public event Action<ServerMessage>? onEvent; - - public readonly Address clientAddress = Address.Random(); - public Identity? clientIdentity { get; private set; } + public readonly Address Address = Address.Random(); + public Identity? Identity { get; private set; } internal WebSocket webSocket; private bool connectionClosed; @@ -148,7 +140,7 @@ struct DbOp protected DbConnectionBase() { - var options = new ConnectOptions + var options = new WebSocket.ConnectOptions { //v1.bin.spacetimedb //v1.text.spacetimedb @@ -470,7 +462,7 @@ ProcessedMessage CalculateStateDiff(PreProcessedMessage preProcessedMessage) return processed; } - public void Close() + public void Disconnect() { isClosing = true; connectionClosed = true; @@ -483,7 +475,7 @@ public void Close() /// </summary> /// <param name="uri"> URI of the SpacetimeDB server (ex: https://testnet.spacetimedb.com) /// <param name="addressOrName">The name or address of the database to connect to</param> - public void Connect(string? token, string uri, string addressOrName) + internal void Connect(string? token, string uri, string addressOrName) { isClosing = false; @@ -499,7 +491,7 @@ public void Connect(string? token, string uri, string addressOrName) { try { - await webSocket.Connect(token, uri, addressOrName, clientAddress); + await webSocket.Connect(token, uri, addressOrName, Address); } catch (Exception e) { @@ -605,7 +597,6 @@ private void OnMessageProcessComplete(PreProcessedMessage preProcessed) { case ServerMessage.InitialSubscription(var initialSubscription): { - onBeforeSubscriptionApplied?.Invoke(); stats.ParseMessageTracker.InsertRequest(timestamp, $"type={nameof(ServerMessage.InitialSubscription)}"); stats.SubscriptionRequestTracker.FinishTrackingRequest(initialSubscription.RequestId); var eventContext = ToEventContext(new Event<Reducer>.SubscribeApplied()); @@ -630,7 +621,7 @@ private void OnMessageProcessComplete(PreProcessedMessage preProcessed) var hostDuration = TimeSpan.FromMilliseconds(transactionUpdate.HostExecutionDurationMicros / 1000.0d); stats.AllReducersTracker.InsertRequest(hostDuration, $"reducer={reducer}"); var callerIdentity = transactionUpdate.CallerIdentity; - if (callerIdentity == clientIdentity) + if (callerIdentity == Identity) { // This was a request that we initiated var requestId = transactionUpdate.ReducerCall.RequestId; @@ -648,14 +639,6 @@ private void OnMessageProcessComplete(PreProcessedMessage preProcessed) var eventContext = ToEventContext(new Event<Reducer>.Reducer(reducerEvent)); OnMessageProcessCompleteUpdate(eventContext, dbOps); - try - { - onEvent?.Invoke(message); - } - catch (Exception e) - { - Log.Exception(e); - } var reducerFound = false; try @@ -683,7 +666,7 @@ private void OnMessageProcessComplete(PreProcessedMessage preProcessed) case ServerMessage.IdentityToken(var identityToken): try { - clientIdentity = identityToken.Identity; + Identity = identityToken.Identity; onConnect?.Invoke(identityToken.Identity, identityToken.Token); } catch (Exception e) @@ -693,14 +676,7 @@ private void OnMessageProcessComplete(PreProcessedMessage preProcessed) break; case ServerMessage.OneOffQueryResponse: - try - { - onEvent?.Invoke(message); - } - catch (Exception e) - { - Log.Exception(e); - } + /* OneOffQuery is async and handles its own responses */ break; default: @@ -807,9 +783,9 @@ T[] LogAndThrow(string error) return resultTable.Rows.Select(BSATNHelpers.Decode<T>).ToArray(); } - public bool IsConnected() => webSocket.IsConnected; + public bool IsActive => webSocket.IsConnected; - public void Update() + public void FrameTick() { webSocket.Update(); while (_preProcessedNetworkMessages.TryTake(out var preProcessedMessage)) diff --git a/src/UnityDebugLogger.cs b/src/UnityDebugLogger.cs index 114079f0..218cde18 100644 --- a/src/UnityDebugLogger.cs +++ b/src/UnityDebugLogger.cs @@ -8,7 +8,7 @@ namespace SpacetimeDB { - public class UnityDebugLogger : ISpacetimeDBLogger + internal class UnityDebugLogger : ISpacetimeDBLogger { public void Debug(string message) { diff --git a/src/WebSocket.cs b/src/WebSocket.cs index 73d23e9a..cd22c26f 100644 --- a/src/WebSocket.cs +++ b/src/WebSocket.cs @@ -11,23 +11,22 @@ namespace SpacetimeDB { - public delegate void WebSocketOpenEventHandler(); - - public delegate void WebSocketMessageEventHandler(byte[] message, DateTime timestamp); + internal class WebSocket + { + public delegate void OpenEventHandler(); - public delegate void WebSocketCloseEventHandler(WebSocketCloseStatus? code, WebSocketError? error); + public delegate void MessageEventHandler(byte[] message, DateTime timestamp); - public delegate void WebSocketConnectErrorEventHandler(WebSocketError? error, string message); - public delegate void WebSocketSendErrorEventHandler(Exception e); + public delegate void CloseEventHandler(WebSocketCloseStatus? code, WebSocketError? error); - public struct ConnectOptions - { - public string Protocol; - } + public delegate void ConnectErrorEventHandler(WebSocketError? error, string message); + public delegate void SendErrorEventHandler(Exception e); + public struct ConnectOptions + { + public string Protocol; + } - public class WebSocket - { // WebSocket buffer for incoming messages private static readonly int MAXMessageSize = 0x4000000; // 64MB @@ -43,11 +42,11 @@ public WebSocket(ConnectOptions options) _options = options; } - public event WebSocketOpenEventHandler? OnConnect; - public event WebSocketConnectErrorEventHandler? OnConnectError; - public event WebSocketSendErrorEventHandler? OnSendError; - public event WebSocketMessageEventHandler? OnMessage; - public event WebSocketCloseEventHandler? OnClose; + public event OpenEventHandler? OnConnect; + public event ConnectErrorEventHandler? OnConnectError; + public event SendErrorEventHandler? OnSendError; + public event MessageEventHandler? OnMessage; + public event CloseEventHandler? OnClose; public bool IsConnected { get { return Ws != null && Ws.State == WebSocketState.Open; } } diff --git a/tests~/SnapshotTests.VerifyAllTablesParsed.verified.txt b/tests~/SnapshotTests.VerifyAllTablesParsed.verified.txt index fd948740..513aa66c 100644 --- a/tests~/SnapshotTests.VerifyAllTablesParsed.verified.txt +++ b/tests~/SnapshotTests.VerifyAllTablesParsed.verified.txt @@ -1,6 +1,10 @@ { Events: { - OnIdentityReceived: Identity_1, + Log: SpacetimeDBClient: Connecting to wss://spacetimedb.com example, + OnConnect: { + identity: Identity_1, + token: eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJoZXhfaWRlbnRpdHkiOiI4ZjkwY2M5NGE5OTY4ZGY2ZDI5N2JhYTY2NTAzYTg5M2IxYzM0YjBiMDAyNjhhNTE0ODk4ZGQ5NTRiMGRhMjBiIiwiaWF0IjoxNzE4NDg3NjY4LCJleHAiOm51bGx9.PSn481bLRqtFwIh46nOXDY14X3GKbz8t4K4GmBmz50loU6xzeL7zDdCh1V2cmiQsoGq8Erxg0r_6b6Y5SqKoBA + }, OnInsertUser: { eventContext: { Reducers: {Scrubbed}, @@ -33,18 +37,6 @@ online: true } }, - OnEvent: { - status: {Scrubbed}, - timestamp: 1718487763059031, - caller_identity: Identity_2, - caller_address: Guid_1, - reducer_call: { - reducer_name: __identity_connected__, - args: - }, - energy_quanta_used: 1957615, - host_execution_duration_micros: 66 - }, OnUpdateUser: { eventContext: { Reducers: {Scrubbed}, @@ -74,19 +66,6 @@ online: true } }, - OnEvent: { - status: {Scrubbed}, - timestamp: 1718487768057579, - caller_identity: Identity_1, - caller_address: Guid_2, - reducer_call: { - reducer_name: set_name, - args: AQAAAEE=, - request_id: 1 - }, - energy_quanta_used: 4345615, - host_execution_duration_micros: 70 - }, OnSetName: { Reducers: {Scrubbed}, Reducer: { @@ -130,19 +109,6 @@ text: Hello, A! } }, - OnEvent: { - status: {Scrubbed}, - timestamp: 1718487775346381, - caller_identity: Identity_2, - caller_address: Guid_1, - reducer_call: { - reducer_name: send_message, - args: CQAAAEhlbGxvLCBBIQ==, - request_id: 1 - }, - energy_quanta_used: 2779615, - host_execution_duration_micros: 57 - }, OnSendMessage: { Reducers: {Scrubbed}, Reducer: { @@ -190,19 +156,6 @@ online: true } }, - OnEvent: { - status: {Scrubbed}, - timestamp: 1718487777307855, - caller_identity: Identity_2, - caller_address: Guid_1, - reducer_call: { - reducer_name: set_name, - args: AQAAAEI=, - request_id: 2 - }, - energy_quanta_used: 4268615, - host_execution_duration_micros: 98 - }, OnSetName: { Reducers: {Scrubbed}, Reducer: { @@ -246,19 +199,6 @@ text: Hello, B! } }, - OnEvent: { - status: {Scrubbed}, - timestamp: 1718487783175083, - caller_identity: Identity_1, - caller_address: Guid_2, - reducer_call: { - reducer_name: send_message, - args: CQAAAEhlbGxvLCBCIQ==, - request_id: 2 - }, - energy_quanta_used: 2677615, - host_execution_duration_micros: 40 - }, OnSendMessage: { Reducers: {Scrubbed}, Reducer: { @@ -302,19 +242,6 @@ text: Goodbye! } }, - OnEvent: { - status: {Scrubbed}, - timestamp: 1718487787645364, - caller_identity: Identity_2, - caller_address: Guid_1, - reducer_call: { - reducer_name: send_message, - args: CAAAAEdvb2RieWUh, - request_id: 3 - }, - energy_quanta_used: 2636615, - host_execution_duration_micros: 28 - }, OnSendMessage: { Reducers: {Scrubbed}, Reducer: { @@ -359,18 +286,6 @@ online: false } }, - OnEvent: { - status: {Scrubbed}, - timestamp: 1718487791901504, - caller_identity: Identity_2, - caller_address: Guid_1, - reducer_call: { - reducer_name: __identity_disconnected__, - args: - }, - energy_quanta_used: 3595615, - host_execution_duration_micros: 75 - }, OnInsertMessage: { eventContext: { Reducers: {Scrubbed}, @@ -396,19 +311,6 @@ text: Goodbye! } }, - OnEvent: { - status: {Scrubbed}, - timestamp: 1718487794937841, - caller_identity: Identity_1, - caller_address: Guid_2, - reducer_call: { - reducer_name: send_message, - args: CAAAAEdvb2RieWUh, - request_id: 3 - }, - energy_quanta_used: 2636615, - host_execution_duration_micros: 34 - }, OnSendMessage: { Reducers: {Scrubbed}, Reducer: { diff --git a/tests~/SnapshotTests.cs b/tests~/SnapshotTests.cs index e99e601c..dd08dfe6 100644 --- a/tests~/SnapshotTests.cs +++ b/tests~/SnapshotTests.cs @@ -258,7 +258,12 @@ public async Task VerifyAllTablesParsed() Log.Current = new TestLogger(events); - var client = new DbConnection(); + var client = + DbConnection.Builder() + .WithUri("wss://spacetimedb.com") + .WithModuleName("example") + .OnConnect((conn, identity, token) => events.Add("OnConnect", new { identity, token })) + .Build(); var sampleDumpParsed = SampleDump(); @@ -286,20 +291,10 @@ public async Task VerifyAllTablesParsed() } ); - client.onBeforeSubscriptionApplied += () => events.Add("OnBeforeSubscriptionApplied"); - client.onEvent += (ev) => events.Add("OnEvent", ev switch - { - ServerMessage.IdentityToken(var o) => o, - ServerMessage.InitialSubscription(var o) => o, - ServerMessage.TransactionUpdate(var o) => o, - ServerMessage.OneOffQueryResponse(var o) => o, - _ => throw new InvalidOperationException() - }); - client.onConnect += (identity, _token) => - events.Add("OnIdentityReceived", identity); +#pragma warning disable CS0612 // Using obsolete API client.onUnhandledReducerError += (exception) => events.Add("OnUnhandledReducerError", exception); - +#pragma warning restore CS0612 // Using obsolete API client.RemoteReducers.OnSendMessage += (eventContext, _text) => events.Add("OnSendMessage", eventContext); client.RemoteReducers.OnSetName += (eventContext, _name) => events.Add("OnSetName", eventContext); @@ -332,7 +327,7 @@ public async Task VerifyAllTablesParsed() // Otherwise we'll get inconsistent output order between test reruns. while (!client.HasPreProcessedMessage) { } // Once the message is in the preprocessed queue, we can invoke Update() to handle events on the main thread. - client.Update(); + client.FrameTick(); } // Verify dumped events and the final client state. diff --git a/tests~/tests.csproj b/tests~/tests.csproj index 23735fb4..804fa736 100644 --- a/tests~/tests.csproj +++ b/tests~/tests.csproj @@ -23,7 +23,7 @@ <PackageReference Include="xunit" Version="2.8.1" /> <ProjectReference Include="../SpacetimeDB.ClientSDK.csproj" /> - <Compile Include="../examples~/quickstart/client/module_bindings/**/*.cs" /> + <ProjectReference Include="../examples~/quickstart/client/client.csproj" /> </ItemGroup> </Project> From 8df6d15a2bf9921acc2004d455e2db403b02aa9d Mon Sep 17 00:00:00 2001 From: Mazdak Farrokhzad <twingoow@gmail.com> Date: Wed, 2 Oct 2024 17:18:24 +0200 Subject: [PATCH 12/55] Implement websocket changes atop C# sdk changes (#136) ## Requires SpacetimeDB PRs This is the C# side of https://github.com/clockworklabs/SpacetimeDB/pull/1761. --- src/BSATNHelpers.cs | 7 - src/SpacetimeDB/ClientApi/BsatnRowList.cs | 40 +++ src/SpacetimeDB/ClientApi/CallReducer.cs | 21 +- .../ClientApi/CompressableQueryUpdate.cs | 17 ++ src/SpacetimeDB/ClientApi/DatabaseUpdate.cs | 14 +- .../ClientApi/EncodedValue.cs.meta | 11 - src/SpacetimeDB/ClientApi/EnergyQuanta.cs | 11 + src/SpacetimeDB/ClientApi/IdentityToken.cs | 24 +- .../ClientApi/InitialSubscription.cs | 18 +- src/SpacetimeDB/ClientApi/OneOffQuery.cs | 19 +- .../ClientApi/OneOffQueryResponse.cs | 23 +- src/SpacetimeDB/ClientApi/OneOffTable.cs | 19 +- src/SpacetimeDB/ClientApi/QueryUpdate.cs | 40 +++ src/SpacetimeDB/ClientApi/ReducerCallInfo.cs | 23 +- .../{EncodedValue.cs => RowSizeHint.cs} | 6 +- src/SpacetimeDB/ClientApi/Subscribe.cs | 16 +- src/SpacetimeDB/ClientApi/TableUpdate.cs | 29 +- src/SpacetimeDB/ClientApi/Timestamp.cs | 11 + .../ClientApi/TransactionUpdate.cs | 41 ++- src/SpacetimeDBClient.cs | 277 ++++++++++++------ ...otTests.VerifyAllTablesParsed.verified.txt | 2 +- tests~/SnapshotTests.cs | 64 ++-- 22 files changed, 567 insertions(+), 166 deletions(-) create mode 100644 src/SpacetimeDB/ClientApi/BsatnRowList.cs create mode 100644 src/SpacetimeDB/ClientApi/CompressableQueryUpdate.cs delete mode 100644 src/SpacetimeDB/ClientApi/EncodedValue.cs.meta create mode 100644 src/SpacetimeDB/ClientApi/QueryUpdate.cs rename src/SpacetimeDB/ClientApi/{EncodedValue.cs => RowSizeHint.cs} (66%) diff --git a/src/BSATNHelpers.cs b/src/BSATNHelpers.cs index 31f15628..46c4cb64 100644 --- a/src/BSATNHelpers.cs +++ b/src/BSATNHelpers.cs @@ -25,12 +25,5 @@ public static T Decode<T>(string json) { throw new InvalidOperationException("JSON isn't supported at the moment"); } - - public static T Decode<T>(EncodedValue value) where T : IStructuralReadWrite, new() => value switch - { - EncodedValue.Binary(var bin) => Decode<T>(bin), - EncodedValue.Text(var text) => Decode<T>(text), - _ => throw new InvalidOperationException() - }; } } diff --git a/src/SpacetimeDB/ClientApi/BsatnRowList.cs b/src/SpacetimeDB/ClientApi/BsatnRowList.cs new file mode 100644 index 00000000..5df43d6b --- /dev/null +++ b/src/SpacetimeDB/ClientApi/BsatnRowList.cs @@ -0,0 +1,40 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN RUST INSTEAD. +// <auto-generated /> + +#nullable enable + +using System; +using SpacetimeDB; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; + +namespace SpacetimeDB.ClientApi +{ + [SpacetimeDB.Type] + [DataContract] + public partial class BsatnRowList + { + [DataMember(Name = "size_hint")] + public SpacetimeDB.ClientApi.RowSizeHint SizeHint; + [DataMember(Name = "rows_data")] + public byte[] RowsData; + + public BsatnRowList( + SpacetimeDB.ClientApi.RowSizeHint SizeHint, + byte[] RowsData + ) + { + this.SizeHint = SizeHint; + this.RowsData = RowsData; + } + + public BsatnRowList() + { + this.SizeHint = null!; + this.RowsData = Array.Empty<byte>(); + } + + } +} diff --git a/src/SpacetimeDB/ClientApi/CallReducer.cs b/src/SpacetimeDB/ClientApi/CallReducer.cs index d3f56f38..7c3ce90e 100644 --- a/src/SpacetimeDB/ClientApi/CallReducer.cs +++ b/src/SpacetimeDB/ClientApi/CallReducer.cs @@ -17,11 +17,28 @@ namespace SpacetimeDB.ClientApi public partial class CallReducer { [DataMember(Name = "reducer")] - public string Reducer = ""; + public string Reducer; [DataMember(Name = "args")] - public SpacetimeDB.ClientApi.EncodedValue Args = null!; + public byte[] Args; [DataMember(Name = "request_id")] public uint RequestId; + public CallReducer( + string Reducer, + byte[] Args, + uint RequestId + ) + { + this.Reducer = Reducer; + this.Args = Args; + this.RequestId = RequestId; + } + + public CallReducer() + { + this.Reducer = ""; + this.Args = Array.Empty<byte>(); + } + } } diff --git a/src/SpacetimeDB/ClientApi/CompressableQueryUpdate.cs b/src/SpacetimeDB/ClientApi/CompressableQueryUpdate.cs new file mode 100644 index 00000000..af397a58 --- /dev/null +++ b/src/SpacetimeDB/ClientApi/CompressableQueryUpdate.cs @@ -0,0 +1,17 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN RUST INSTEAD. +// <auto-generated /> + +#nullable enable + +using System; +using SpacetimeDB; + +namespace SpacetimeDB.ClientApi +{ + [SpacetimeDB.Type] + public partial record CompressableQueryUpdate : SpacetimeDB.TaggedEnum<( + SpacetimeDB.ClientApi.QueryUpdate Uncompressed, + byte[] Brotli + )>; +} diff --git a/src/SpacetimeDB/ClientApi/DatabaseUpdate.cs b/src/SpacetimeDB/ClientApi/DatabaseUpdate.cs index 245117ad..3294c6b7 100644 --- a/src/SpacetimeDB/ClientApi/DatabaseUpdate.cs +++ b/src/SpacetimeDB/ClientApi/DatabaseUpdate.cs @@ -17,7 +17,19 @@ namespace SpacetimeDB.ClientApi public partial class DatabaseUpdate { [DataMember(Name = "tables")] - public System.Collections.Generic.List<SpacetimeDB.ClientApi.TableUpdate> Tables = new(); + public System.Collections.Generic.List<SpacetimeDB.ClientApi.TableUpdate> Tables; + + public DatabaseUpdate( + System.Collections.Generic.List<SpacetimeDB.ClientApi.TableUpdate> Tables + ) + { + this.Tables = Tables; + } + + public DatabaseUpdate() + { + this.Tables = new(); + } } } diff --git a/src/SpacetimeDB/ClientApi/EncodedValue.cs.meta b/src/SpacetimeDB/ClientApi/EncodedValue.cs.meta deleted file mode 100644 index c89e37b1..00000000 --- a/src/SpacetimeDB/ClientApi/EncodedValue.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 16581799d308744cbba931760b17b64c -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/src/SpacetimeDB/ClientApi/EnergyQuanta.cs b/src/SpacetimeDB/ClientApi/EnergyQuanta.cs index ba142cb3..476712e9 100644 --- a/src/SpacetimeDB/ClientApi/EnergyQuanta.cs +++ b/src/SpacetimeDB/ClientApi/EnergyQuanta.cs @@ -19,5 +19,16 @@ public partial class EnergyQuanta [DataMember(Name = "quanta")] public U128 Quanta; + public EnergyQuanta( + U128 Quanta + ) + { + this.Quanta = Quanta; + } + + public EnergyQuanta() + { + } + } } diff --git a/src/SpacetimeDB/ClientApi/IdentityToken.cs b/src/SpacetimeDB/ClientApi/IdentityToken.cs index 344cbbba..4a53ad2f 100644 --- a/src/SpacetimeDB/ClientApi/IdentityToken.cs +++ b/src/SpacetimeDB/ClientApi/IdentityToken.cs @@ -17,11 +17,29 @@ namespace SpacetimeDB.ClientApi public partial class IdentityToken { [DataMember(Name = "identity")] - public SpacetimeDB.Identity Identity = new(); + public SpacetimeDB.Identity Identity; [DataMember(Name = "token")] - public string Token = ""; + public string Token; [DataMember(Name = "address")] - public SpacetimeDB.Address Address = new(); + public SpacetimeDB.Address Address; + + public IdentityToken( + SpacetimeDB.Identity Identity, + string Token, + SpacetimeDB.Address Address + ) + { + this.Identity = Identity; + this.Token = Token; + this.Address = Address; + } + + public IdentityToken() + { + this.Identity = new(); + this.Token = ""; + this.Address = new(); + } } } diff --git a/src/SpacetimeDB/ClientApi/InitialSubscription.cs b/src/SpacetimeDB/ClientApi/InitialSubscription.cs index 06ae7ba8..cdbc73de 100644 --- a/src/SpacetimeDB/ClientApi/InitialSubscription.cs +++ b/src/SpacetimeDB/ClientApi/InitialSubscription.cs @@ -17,11 +17,27 @@ namespace SpacetimeDB.ClientApi public partial class InitialSubscription { [DataMember(Name = "database_update")] - public SpacetimeDB.ClientApi.DatabaseUpdate DatabaseUpdate = new(); + public SpacetimeDB.ClientApi.DatabaseUpdate DatabaseUpdate; [DataMember(Name = "request_id")] public uint RequestId; [DataMember(Name = "total_host_execution_duration_micros")] public ulong TotalHostExecutionDurationMicros; + public InitialSubscription( + SpacetimeDB.ClientApi.DatabaseUpdate DatabaseUpdate, + uint RequestId, + ulong TotalHostExecutionDurationMicros + ) + { + this.DatabaseUpdate = DatabaseUpdate; + this.RequestId = RequestId; + this.TotalHostExecutionDurationMicros = TotalHostExecutionDurationMicros; + } + + public InitialSubscription() + { + this.DatabaseUpdate = new(); + } + } } diff --git a/src/SpacetimeDB/ClientApi/OneOffQuery.cs b/src/SpacetimeDB/ClientApi/OneOffQuery.cs index c103c829..5ee2c66d 100644 --- a/src/SpacetimeDB/ClientApi/OneOffQuery.cs +++ b/src/SpacetimeDB/ClientApi/OneOffQuery.cs @@ -17,9 +17,24 @@ namespace SpacetimeDB.ClientApi public partial class OneOffQuery { [DataMember(Name = "message_id")] - public byte[] MessageId = Array.Empty<byte>(); + public byte[] MessageId; [DataMember(Name = "query_string")] - public string QueryString = ""; + public string QueryString; + + public OneOffQuery( + byte[] MessageId, + string QueryString + ) + { + this.MessageId = MessageId; + this.QueryString = QueryString; + } + + public OneOffQuery() + { + this.MessageId = Array.Empty<byte>(); + this.QueryString = ""; + } } } diff --git a/src/SpacetimeDB/ClientApi/OneOffQueryResponse.cs b/src/SpacetimeDB/ClientApi/OneOffQueryResponse.cs index 35c1d0eb..f539516f 100644 --- a/src/SpacetimeDB/ClientApi/OneOffQueryResponse.cs +++ b/src/SpacetimeDB/ClientApi/OneOffQueryResponse.cs @@ -17,13 +17,32 @@ namespace SpacetimeDB.ClientApi public partial class OneOffQueryResponse { [DataMember(Name = "message_id")] - public byte[] MessageId = Array.Empty<byte>(); + public byte[] MessageId; [DataMember(Name = "error")] public string? Error; [DataMember(Name = "tables")] - public System.Collections.Generic.List<SpacetimeDB.ClientApi.OneOffTable> Tables = new(); + public System.Collections.Generic.List<SpacetimeDB.ClientApi.OneOffTable> Tables; [DataMember(Name = "total_host_execution_duration_micros")] public ulong TotalHostExecutionDurationMicros; + public OneOffQueryResponse( + byte[] MessageId, + string? Error, + System.Collections.Generic.List<SpacetimeDB.ClientApi.OneOffTable> Tables, + ulong TotalHostExecutionDurationMicros + ) + { + this.MessageId = MessageId; + this.Error = Error; + this.Tables = Tables; + this.TotalHostExecutionDurationMicros = TotalHostExecutionDurationMicros; + } + + public OneOffQueryResponse() + { + this.MessageId = Array.Empty<byte>(); + this.Tables = new(); + } + } } diff --git a/src/SpacetimeDB/ClientApi/OneOffTable.cs b/src/SpacetimeDB/ClientApi/OneOffTable.cs index 12f7e0f9..d1f48f3f 100644 --- a/src/SpacetimeDB/ClientApi/OneOffTable.cs +++ b/src/SpacetimeDB/ClientApi/OneOffTable.cs @@ -17,9 +17,24 @@ namespace SpacetimeDB.ClientApi public partial class OneOffTable { [DataMember(Name = "table_name")] - public string TableName = ""; + public string TableName; [DataMember(Name = "rows")] - public System.Collections.Generic.List<SpacetimeDB.ClientApi.EncodedValue> Rows = new(); + public SpacetimeDB.ClientApi.BsatnRowList Rows; + + public OneOffTable( + string TableName, + SpacetimeDB.ClientApi.BsatnRowList Rows + ) + { + this.TableName = TableName; + this.Rows = Rows; + } + + public OneOffTable() + { + this.TableName = ""; + this.Rows = new(); + } } } diff --git a/src/SpacetimeDB/ClientApi/QueryUpdate.cs b/src/SpacetimeDB/ClientApi/QueryUpdate.cs new file mode 100644 index 00000000..858dea65 --- /dev/null +++ b/src/SpacetimeDB/ClientApi/QueryUpdate.cs @@ -0,0 +1,40 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN RUST INSTEAD. +// <auto-generated /> + +#nullable enable + +using System; +using SpacetimeDB; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; + +namespace SpacetimeDB.ClientApi +{ + [SpacetimeDB.Type] + [DataContract] + public partial class QueryUpdate + { + [DataMember(Name = "deletes")] + public SpacetimeDB.ClientApi.BsatnRowList Deletes; + [DataMember(Name = "inserts")] + public SpacetimeDB.ClientApi.BsatnRowList Inserts; + + public QueryUpdate( + SpacetimeDB.ClientApi.BsatnRowList Deletes, + SpacetimeDB.ClientApi.BsatnRowList Inserts + ) + { + this.Deletes = Deletes; + this.Inserts = Inserts; + } + + public QueryUpdate() + { + this.Deletes = new(); + this.Inserts = new(); + } + + } +} diff --git a/src/SpacetimeDB/ClientApi/ReducerCallInfo.cs b/src/SpacetimeDB/ClientApi/ReducerCallInfo.cs index 44bc8865..0ca52252 100644 --- a/src/SpacetimeDB/ClientApi/ReducerCallInfo.cs +++ b/src/SpacetimeDB/ClientApi/ReducerCallInfo.cs @@ -17,13 +17,32 @@ namespace SpacetimeDB.ClientApi public partial class ReducerCallInfo { [DataMember(Name = "reducer_name")] - public string ReducerName = ""; + public string ReducerName; [DataMember(Name = "reducer_id")] public uint ReducerId; [DataMember(Name = "args")] - public SpacetimeDB.ClientApi.EncodedValue Args = null!; + public byte[] Args; [DataMember(Name = "request_id")] public uint RequestId; + public ReducerCallInfo( + string ReducerName, + uint ReducerId, + byte[] Args, + uint RequestId + ) + { + this.ReducerName = ReducerName; + this.ReducerId = ReducerId; + this.Args = Args; + this.RequestId = RequestId; + } + + public ReducerCallInfo() + { + this.ReducerName = ""; + this.Args = Array.Empty<byte>(); + } + } } diff --git a/src/SpacetimeDB/ClientApi/EncodedValue.cs b/src/SpacetimeDB/ClientApi/RowSizeHint.cs similarity index 66% rename from src/SpacetimeDB/ClientApi/EncodedValue.cs rename to src/SpacetimeDB/ClientApi/RowSizeHint.cs index 16793ca5..71c04c1b 100644 --- a/src/SpacetimeDB/ClientApi/EncodedValue.cs +++ b/src/SpacetimeDB/ClientApi/RowSizeHint.cs @@ -10,8 +10,8 @@ namespace SpacetimeDB.ClientApi { [SpacetimeDB.Type] - public partial record EncodedValue : SpacetimeDB.TaggedEnum<( - byte[] Binary, - string Text + public partial record RowSizeHint : SpacetimeDB.TaggedEnum<( + ushort FixedSize, + System.Collections.Generic.List<ulong> RowOffsets )>; } diff --git a/src/SpacetimeDB/ClientApi/Subscribe.cs b/src/SpacetimeDB/ClientApi/Subscribe.cs index d5eaba81..0adad9b2 100644 --- a/src/SpacetimeDB/ClientApi/Subscribe.cs +++ b/src/SpacetimeDB/ClientApi/Subscribe.cs @@ -17,9 +17,23 @@ namespace SpacetimeDB.ClientApi public partial class Subscribe { [DataMember(Name = "query_strings")] - public System.Collections.Generic.List<string> QueryStrings = new(); + public System.Collections.Generic.List<string> QueryStrings; [DataMember(Name = "request_id")] public uint RequestId; + public Subscribe( + System.Collections.Generic.List<string> QueryStrings, + uint RequestId + ) + { + this.QueryStrings = QueryStrings; + this.RequestId = RequestId; + } + + public Subscribe() + { + this.QueryStrings = new(); + } + } } diff --git a/src/SpacetimeDB/ClientApi/TableUpdate.cs b/src/SpacetimeDB/ClientApi/TableUpdate.cs index b9b47ea5..cc2586d3 100644 --- a/src/SpacetimeDB/ClientApi/TableUpdate.cs +++ b/src/SpacetimeDB/ClientApi/TableUpdate.cs @@ -19,11 +19,30 @@ public partial class TableUpdate [DataMember(Name = "table_id")] public uint TableId; [DataMember(Name = "table_name")] - public string TableName = ""; - [DataMember(Name = "deletes")] - public System.Collections.Generic.List<SpacetimeDB.ClientApi.EncodedValue> Deletes = new(); - [DataMember(Name = "inserts")] - public System.Collections.Generic.List<SpacetimeDB.ClientApi.EncodedValue> Inserts = new(); + public string TableName; + [DataMember(Name = "num_rows")] + public ulong NumRows; + [DataMember(Name = "updates")] + public System.Collections.Generic.List<SpacetimeDB.ClientApi.CompressableQueryUpdate> Updates; + + public TableUpdate( + uint TableId, + string TableName, + ulong NumRows, + System.Collections.Generic.List<SpacetimeDB.ClientApi.CompressableQueryUpdate> Updates + ) + { + this.TableId = TableId; + this.TableName = TableName; + this.NumRows = NumRows; + this.Updates = Updates; + } + + public TableUpdate() + { + this.TableName = ""; + this.Updates = new(); + } } } diff --git a/src/SpacetimeDB/ClientApi/Timestamp.cs b/src/SpacetimeDB/ClientApi/Timestamp.cs index 4dd2535c..c0f53ba1 100644 --- a/src/SpacetimeDB/ClientApi/Timestamp.cs +++ b/src/SpacetimeDB/ClientApi/Timestamp.cs @@ -19,5 +19,16 @@ public partial class Timestamp [DataMember(Name = "microseconds")] public ulong Microseconds; + public Timestamp( + ulong Microseconds + ) + { + this.Microseconds = Microseconds; + } + + public Timestamp() + { + } + } } diff --git a/src/SpacetimeDB/ClientApi/TransactionUpdate.cs b/src/SpacetimeDB/ClientApi/TransactionUpdate.cs index fb71dd9e..e4925bf4 100644 --- a/src/SpacetimeDB/ClientApi/TransactionUpdate.cs +++ b/src/SpacetimeDB/ClientApi/TransactionUpdate.cs @@ -17,19 +17,48 @@ namespace SpacetimeDB.ClientApi public partial class TransactionUpdate { [DataMember(Name = "status")] - public SpacetimeDB.ClientApi.UpdateStatus Status = null!; + public SpacetimeDB.ClientApi.UpdateStatus Status; [DataMember(Name = "timestamp")] - public SpacetimeDB.ClientApi.Timestamp Timestamp = new(); + public SpacetimeDB.ClientApi.Timestamp Timestamp; [DataMember(Name = "caller_identity")] - public SpacetimeDB.Identity CallerIdentity = new(); + public SpacetimeDB.Identity CallerIdentity; [DataMember(Name = "caller_address")] - public SpacetimeDB.Address CallerAddress = new(); + public SpacetimeDB.Address CallerAddress; [DataMember(Name = "reducer_call")] - public SpacetimeDB.ClientApi.ReducerCallInfo ReducerCall = new(); + public SpacetimeDB.ClientApi.ReducerCallInfo ReducerCall; [DataMember(Name = "energy_quanta_used")] - public SpacetimeDB.ClientApi.EnergyQuanta EnergyQuantaUsed = new(); + public SpacetimeDB.ClientApi.EnergyQuanta EnergyQuantaUsed; [DataMember(Name = "host_execution_duration_micros")] public ulong HostExecutionDurationMicros; + public TransactionUpdate( + SpacetimeDB.ClientApi.UpdateStatus Status, + SpacetimeDB.ClientApi.Timestamp Timestamp, + SpacetimeDB.Identity CallerIdentity, + SpacetimeDB.Address CallerAddress, + SpacetimeDB.ClientApi.ReducerCallInfo ReducerCall, + SpacetimeDB.ClientApi.EnergyQuanta EnergyQuantaUsed, + ulong HostExecutionDurationMicros + ) + { + this.Status = Status; + this.Timestamp = Timestamp; + this.CallerIdentity = CallerIdentity; + this.CallerAddress = CallerAddress; + this.ReducerCall = ReducerCall; + this.EnergyQuantaUsed = EnergyQuantaUsed; + this.HostExecutionDurationMicros = HostExecutionDurationMicros; + } + + public TransactionUpdate() + { + this.Status = null!; + this.Timestamp = new(); + this.CallerIdentity = new(); + this.CallerAddress = new(); + this.ReducerCall = new(); + this.EnergyQuantaUsed = new(); + } + } } diff --git a/src/SpacetimeDBClient.cs b/src/SpacetimeDBClient.cs index 88c85ccc..c01bb9b9 100644 --- a/src/SpacetimeDBClient.cs +++ b/src/SpacetimeDBClient.cs @@ -144,7 +144,7 @@ protected DbConnectionBase() { //v1.bin.spacetimedb //v1.text.spacetimedb - Protocol = "v1.bin.spacetimedb", + Protocol = "v1.bsatn.spacetimedb" }; webSocket = new WebSocket(options); webSocket.OnMessage += OnMessageReceived; @@ -185,10 +185,8 @@ struct PreProcessedMessage private readonly CancellationTokenSource _preProcessCancellationTokenSource = new(); private CancellationToken _preProcessCancellationToken => _preProcessCancellationTokenSource.Token; - static DbValue Decode(IRemoteTableHandle table, EncodedValue value, out object? primaryKey) + static DbValue Decode(IRemoteTableHandle table, byte[] bin, out object? primaryKey) { - // We expect only binary messages here; let type cast exception take care of any others. - var bin = ((EncodedValue.Binary)value).Binary_; var obj = table.DecodeValue(bin); primaryKey = table.GetPrimaryKey(obj); return new(obj, bin); @@ -197,6 +195,104 @@ static DbValue Decode(IRemoteTableHandle table, EncodedValue value, out object? private static readonly Status Committed = new Status.Committed(default); private static readonly Status OutOfEnergy = new Status.OutOfEnergy(default); + enum CompressionAlgos : byte + { + None = 0, + Brotli = 1, + } + + private static ServerMessage DecompressDecodeMessage(byte[] bytes) + { + using var stream = new MemoryStream(bytes, 1, bytes.Length - 1); + + // The stream will never be empty. It will at least contain the compression algo. + var compression = (CompressionAlgos)bytes[0]; + // Conditionally decompress and decode. + switch (compression) + { + case CompressionAlgos.None: + { + using var binaryReader = new BinaryReader(stream); + return new ServerMessage.BSATN().Read(binaryReader); + } + case CompressionAlgos.Brotli: + { + using var decompressedStream = new BrotliStream(stream, CompressionMode.Decompress); + using var binaryReader = new BinaryReader(decompressedStream); + return new ServerMessage.BSATN().Read(binaryReader); + } + default: + throw new InvalidOperationException("Unknown compression type"); + } + } + + private static QueryUpdate DecompressDecodeQueryUpdate(CompressableQueryUpdate update) + { + switch (update) + { + case CompressableQueryUpdate.Uncompressed(var qu): + return qu; + + case CompressableQueryUpdate.Brotli(var bytes): + { + using var stream = new MemoryStream(bytes); + using var decompressedStream = new BrotliStream(stream, CompressionMode.Decompress); + using var binaryReader = new BinaryReader(decompressedStream); + return new QueryUpdate.BSATN().Read(binaryReader); + } + default: + throw new InvalidOperationException(); + } + } + + private static int BsatnRowListCount(BsatnRowList list) + { + switch (list.SizeHint) + { + case RowSizeHint.FixedSize(var size): + return list.RowsData.Length / size; + case RowSizeHint.RowOffsets(var offsets): + return offsets.Count; + default: + throw new InvalidOperationException("Unknown RowSizeHint variant"); + } + } + + private static IEnumerable<byte[]> BsatnRowListIter(BsatnRowList list) + { + var count = BsatnRowListCount(list); + for (int index = 0; index < count; index += 1) + { + switch (list.SizeHint) + { + case RowSizeHint.FixedSize(var size): + { + int start = index * size; + int elemLen = size; + yield return new ReadOnlySpan<byte>(list.RowsData, start, elemLen).ToArray(); + break; + } + case RowSizeHint.RowOffsets(var offsets): + { + int start = (int)offsets[index]; + // The end is either the start of the next element or the end. + int end; + if (index + 1 == count) + { + end = list.RowsData.Length; + } + else + { + end = (int)offsets[index + 1]; + } + int elemLen = end - start; + yield return new ReadOnlyMemory<byte>(list.RowsData, start, elemLen).ToArray(); + break; + } + } + } + } + void PreProcessMessages() { while (!isClosing) @@ -216,10 +312,8 @@ void PreProcessMessages() PreProcessedMessage PreProcessMessage(UnprocessedMessage unprocessed) { var dbOps = new List<DbOp>(); - using var compressedStream = new MemoryStream(unprocessed.bytes); - using var decompressedStream = new BrotliStream(compressedStream, CompressionMode.Decompress); - using var binaryReader = new BinaryReader(decompressedStream); - var message = new ServerMessage.BSATN().Read(binaryReader); + + var message = DecompressDecodeMessage(unprocessed.bytes); ReducerEvent<Reducer>? reducerEvent = default; @@ -242,7 +336,8 @@ HashSet<byte[]> GetInsertHashSet(System.Type tableType, int tableSize) switch (message) { case ServerMessage.InitialSubscription(var initialSubscription): - subscriptionInserts = new(capacity: initialSubscription.DatabaseUpdate.Tables.Sum(a => a.Inserts.Count)); + int cap = initialSubscription.DatabaseUpdate.Tables.Sum(a => (int)a.NumRows); + subscriptionInserts = new(capacity: cap); // First apply all of the state foreach (var update in initialSubscription.DatabaseUpdate.Tables) @@ -255,37 +350,33 @@ HashSet<byte[]> GetInsertHashSet(System.Type tableType, int tableSize) continue; } - if (update.Deletes.Count != 0) - { - Log.Warn("Non-insert during a subscription update!"); - } - - var hashSet = GetInsertHashSet(table.ClientTableType, initialSubscription.DatabaseUpdate.Tables.Count); + var hashSet = GetInsertHashSet(table.ClientTableType, (int)update.NumRows); - foreach (var row in update.Inserts) + foreach (var cqu in update.Updates) { - switch (row) + var qu = DecompressDecodeQueryUpdate(cqu); + if (BsatnRowListCount(qu.Deletes) != 0) { - case EncodedValue.Binary(var bin): - if (!hashSet.Add(bin)) - { - // Ignore duplicate inserts in the same subscription update. - continue; - } + Log.Warn("Non-insert during a subscription update!"); + } - var obj = table.DecodeValue(bin); - var op = new DbOp - { - table = table, - insert = new(obj, bin), - }; + foreach (var bin in BsatnRowListIter(qu.Inserts)) + { + if (!hashSet.Add(bin)) + { + // Ignore duplicate inserts in the same subscription update. + continue; + } - dbOps.Add(op); - break; + var obj = table.DecodeValue(bin); + var op = new DbOp + { + table = table, + insert = new(obj, bin), + }; - case EncodedValue.Text(var txt): - Log.Warn("JavaScript messages are unsupported."); - break; + dbOps.Add(op); + break; } } } @@ -329,73 +420,77 @@ HashSet<byte[]> GetInsertHashSet(System.Type tableType, int tableSize) continue; } - foreach (var row in update.Inserts) + foreach (var cqu in update.Updates) { - var op = new DbOp { table = table, insert = Decode(table, row, out var pk) }; - if (pk != null) + var qu = DecompressDecodeQueryUpdate(cqu); + foreach (var row in BsatnRowListIter(qu.Inserts)) { - // Compound key that we use for lookup. - // Consists of type of the table (for faster comparison that string names) + actual primary key of the row. - var key = (table.ClientTableType, pk); - - if (primaryKeyChanges.TryGetValue(key, out var oldOp)) + var op = new DbOp { table = table, insert = Decode(table, row, out var pk) }; + if (pk != null) { - if ((op.insert is not null && oldOp.insert is not null) || (op.delete is not null && oldOp.delete is not null)) - { - Log.Warn($"Update with the same primary key was applied multiple times! tableName={tableName}"); - // TODO(jdetter): Is this a correctable error? This would be a major error on the - // SpacetimeDB side. - continue; - } + // Compound key that we use for lookup. + // Consists of type of the table (for faster comparison that string names) + actual primary key of the row. + var key = (table.ClientTableType, pk); - var (insertOp, deleteOp) = op.insert is not null ? (op, oldOp) : (oldOp, op); - op = new DbOp + if (primaryKeyChanges.TryGetValue(key, out var oldOp)) { - table = insertOp.table, - delete = deleteOp.delete, - insert = insertOp.insert, - }; + if ((op.insert is not null && oldOp.insert is not null) || (op.delete is not null && oldOp.delete is not null)) + { + Log.Warn($"Update with the same primary key was applied multiple times! tableName={tableName}"); + // TODO(jdetter): Is this a correctable error? This would be a major error on the + // SpacetimeDB side. + continue; + } + + var (insertOp, deleteOp) = op.insert is not null ? (op, oldOp) : (oldOp, op); + op = new DbOp + { + table = insertOp.table, + delete = deleteOp.delete, + insert = insertOp.insert, + }; + } + primaryKeyChanges[key] = op; + } + else + { + dbOps.Add(op); } - primaryKeyChanges[key] = op; - } - else - { - dbOps.Add(op); } - } - foreach (var row in update.Deletes) - { - var op = new DbOp { table = table, delete = Decode(table, row, out var pk) }; - if (pk != null) + foreach (var row in BsatnRowListIter(qu.Deletes)) { - // Compound key that we use for lookup. - // Consists of type of the table (for faster comparison that string names) + actual primary key of the row. - var key = (table.ClientTableType, pk); - - if (primaryKeyChanges.TryGetValue(key, out var oldOp)) + var op = new DbOp { table = table, delete = Decode(table, row, out var pk) }; + if (pk != null) { - if ((op.insert is not null && oldOp.insert is not null) || (op.delete is not null && oldOp.delete is not null)) - { - Log.Warn($"Update with the same primary key was applied multiple times! tableName={tableName}"); - // TODO(jdetter): Is this a correctable error? This would be a major error on the - // SpacetimeDB side. - continue; - } + // Compound key that we use for lookup. + // Consists of type of the table (for faster comparison that string names) + actual primary key of the row. + var key = (table.ClientTableType, pk); - var (insertOp, deleteOp) = op.insert is not null ? (op, oldOp) : (oldOp, op); - op = new DbOp + if (primaryKeyChanges.TryGetValue(key, out var oldOp)) { - table = insertOp.table, - delete = deleteOp.delete, - insert = insertOp.insert, - }; + if ((op.insert is not null && oldOp.insert is not null) || (op.delete is not null && oldOp.delete is not null)) + { + Log.Warn($"Update with the same primary key was applied multiple times! tableName={tableName}"); + // TODO(jdetter): Is this a correctable error? This would be a major error on the + // SpacetimeDB side. + continue; + } + + var (insertOp, deleteOp) = op.insert is not null ? (op, oldOp) : (oldOp, op); + op = new DbOp + { + table = insertOp.table, + delete = deleteOp.delete, + insert = insertOp.insert, + }; + } + primaryKeyChanges[key] = op; + } + else + { + dbOps.Add(op); } - primaryKeyChanges[key] = op; - } - else - { - dbOps.Add(op); } } } @@ -702,7 +797,7 @@ public void InternalCallReducer<T>(T args) { RequestId = stats.ReducerRequestTracker.StartTrackingRequest(args.ReducerName), Reducer = args.ReducerName, - Args = new EncodedValue.Binary(IStructuralReadWrite.ToBytes(args)) + Args = IStructuralReadWrite.ToBytes(args) } )); } @@ -780,7 +875,9 @@ T[] LogAndThrow(string error) return LogAndThrow($"Mismatched result type, expected {type} but got {resultTable.TableName}"); } - return resultTable.Rows.Select(BSATNHelpers.Decode<T>).ToArray(); + return BsatnRowListIter(resultTable.Rows) + .Select(row => BSATNHelpers.Decode<T>(row)) + .ToArray(); } public bool IsActive => webSocket.IsConnected; diff --git a/tests~/SnapshotTests.VerifyAllTablesParsed.verified.txt b/tests~/SnapshotTests.VerifyAllTablesParsed.verified.txt index 513aa66c..53528975 100644 --- a/tests~/SnapshotTests.VerifyAllTablesParsed.verified.txt +++ b/tests~/SnapshotTests.VerifyAllTablesParsed.verified.txt @@ -390,4 +390,4 @@ Max: type=InitialSubscription } } -} \ No newline at end of file +} diff --git a/tests~/SnapshotTests.cs b/tests~/SnapshotTests.cs index dd08dfe6..c429d07b 100644 --- a/tests~/SnapshotTests.cs +++ b/tests~/SnapshotTests.cs @@ -47,14 +47,6 @@ public override void Write(VerifyJsonWriter writer, EnergyQuanta value) } } - class EncodedValueConverter : WriteOnlyJsonConverter<EncodedValue.Binary> - { - public override void Write(VerifyJsonWriter writer, EncodedValue.Binary value) - { - writer.WriteValue(value.Binary_); - } - } - class TestLogger(Events events) : ISpacetimeDBLogger { public void Debug(string message) @@ -124,7 +116,7 @@ private static ServerMessage.TransactionUpdate SampleTransactionUpdate( ulong energyQuantaUsed, ulong hostExecutionDuration, List<TableUpdate> updates, - EncodedValue? args + byte[]? args ) => new(new() { Timestamp = new Timestamp { Microseconds = timestamp }, @@ -139,7 +131,7 @@ private static ServerMessage.TransactionUpdate SampleTransactionUpdate( { RequestId = requestId, ReducerName = reducerName, - Args = args ?? new EncodedValue.Binary([]) + Args = args ?? [] }, Status = new UpdateStatus.Committed(new() { @@ -147,55 +139,73 @@ private static ServerMessage.TransactionUpdate SampleTransactionUpdate( }) }); - private static TableUpdate SampleUpdate( + private static TableUpdate SampleUpdate<T>( uint tableId, string tableName, - List<EncodedValue> inserts, - List<EncodedValue> deletes - ) => new() + List<T> inserts, + List<T> deletes + ) where T : IStructuralReadWrite => new() { TableId = tableId, TableName = tableName, - Inserts = inserts, - Deletes = deletes + NumRows = (ulong)(inserts.Count + deletes.Count), + Updates = [new CompressableQueryUpdate.Uncompressed(new QueryUpdate( + EncodeRowList<T>(deletes), EncodeRowList<T>(inserts)))] }; - private static EncodedValue.Binary Encode<T>(in T value) where T : IStructuralReadWrite + private static BsatnRowList EncodeRowList<T>(in List<T> list) where T : IStructuralReadWrite + { + var offsets = new List<ulong>(); + var stream = new MemoryStream(); + var writer = new BinaryWriter(stream); + foreach (var elem in list) + { + offsets.Add((ulong)stream.Length); + elem.WriteFields(writer); + } + return new BsatnRowList + { + RowsData = stream.ToArray(), + SizeHint = new RowSizeHint.RowOffsets(offsets) + }; + } + + private static byte[] Encode<T>(in T value) where T : IStructuralReadWrite { var o = new MemoryStream(); var w = new BinaryWriter(o); value.WriteFields(w); - return new EncodedValue.Binary(o.ToArray()); + return o.ToArray(); } private static TableUpdate SampleUserInsert(string identity, string? name, bool online) => - SampleUpdate(4097, "User", [Encode(new User + SampleUpdate(4097, "User", [new User { Identity = Identity.From(Convert.FromBase64String(identity)), Name = name, Online = online - })], []); + }], []); private static TableUpdate SampleUserUpdate(string identity, string? oldName, string? newName, bool oldOnline, bool newOnline) => - SampleUpdate(4097, "User", [Encode(new User + SampleUpdate(4097, "User", [new User { Identity = Identity.From(Convert.FromBase64String(identity)), Name = newName, Online = newOnline - })], [Encode(new User + }], [new User { Identity = Identity.From(Convert.FromBase64String(identity)), Name = oldName, Online = oldOnline - })]); + }]); private static TableUpdate SampleMessage(string identity, ulong sent, string text) => - SampleUpdate(4098, "Message", [Encode(new Message + SampleUpdate(4098, "Message", [new Message { Sender = Identity.From(Convert.FromBase64String(identity)), Sent = sent, Text = text - })], []); + }], []); private static ServerMessage[] SampleDump() => [ SampleId( @@ -282,6 +292,7 @@ public async Task VerifyAllTablesParsed() break; } using var output = new MemoryStream(); + output.WriteByte(1); // Write compression tag. using (var brotli = new BrotliStream(output, CompressionMode.Compress)) { using var w = new BinaryWriter(brotli); @@ -346,8 +357,7 @@ await Verify( .AddExtraSettings(settings => settings.Converters.AddRange([ new EventsConverter(), new TimestampConverter(), - new EnergyQuantaConverter(), - new EncodedValueConverter() + new EnergyQuantaConverter() ])) .ScrubMember<TransactionUpdate>(x => x.Status) .ScrubMember<DbContext<RemoteTables>>(x => x.Db) From 63e6f79da0926114c373dbb2c1ef3a3be74ce223 Mon Sep 17 00:00:00 2001 From: Ingvar Stepanyan <me@rreverser.com> Date: Wed, 2 Oct 2024 21:56:57 +0100 Subject: [PATCH 13/55] Fix UnityDebugLogger implementation (#143) ## Description of Changes Without explicit reference these result in > error CS0119: 'UnityDebugLogger.Debug(string)' is a method, which is not valid in the given context ## API - [ ] This is an API breaking change to the SDK *If the API is breaking, please state below what will break* ## Requires SpacetimeDB PRs *List any PRs here that are required for this SDK change to work* --- src/UnityDebugLogger.cs | 43 ++++++++++++++--------------------------- 1 file changed, 14 insertions(+), 29 deletions(-) diff --git a/src/UnityDebugLogger.cs b/src/UnityDebugLogger.cs index 218cde18..94d21aee 100644 --- a/src/UnityDebugLogger.cs +++ b/src/UnityDebugLogger.cs @@ -4,46 +4,31 @@ */ #if UNITY_5_3_OR_NEWER using System; -using UnityEngine; namespace SpacetimeDB { internal class UnityDebugLogger : ISpacetimeDBLogger { - public void Debug(string message) - { - Debug.Log(message); - } + public void Debug(string message) => + UnityEngine.Debug.Log(message); - public void Trace(string message) - { - Debug.Log(message); - } + public void Trace(string message) => + UnityEngine.Debug.Log(message); - public void Info(string message) - { - Debug.Log(message); - } + public void Info(string message) => + UnityEngine.Debug.Log(message); - public void Warn(string message) - { - Debug.LogWarning(message); - } + public void Warn(string message) => + UnityEngine.Debug.LogWarning(message); - public void Error(string message) - { - Debug.LogError(message); - } + public void Error(string message) => + UnityEngine.Debug.LogError(message); - public void Exception(string message) - { - Debug.LogError(message); - } + public void Exception(string message) => + UnityEngine.Debug.LogError(message); - public void Exception(Exception e) - { - Debug.LogException(e); - } + public void Exception(Exception e) => + UnityEngine.Debug.LogException(e); } } #endif From 2bed3c3dde7c7e3d676821d37ae44b3076c34a01 Mon Sep 17 00:00:00 2001 From: Ingvar Stepanyan <me@rreverser.com> Date: Wed, 2 Oct 2024 22:52:03 +0100 Subject: [PATCH 14/55] Tighten package sources in C# smoketests (#133) ## Description of Changes Same as https://github.com/clockworklabs/SpacetimeDB/pull/1735 but for this repo. ## API - [ ] This is an API breaking change to the SDK *If the API is breaking, please state below what will break* ## Requires SpacetimeDB PRs *List any PRs here that are required for this SDK change to work* --- .github/workflows/dotnet.yml | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 9733140e..69d6b8b1 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -44,9 +44,18 @@ jobs: <packageSources> <!-- Local NuGet repositories --> <add key="Local SpacetimeDB.BSATN.Runtime" value="../SpacetimeDB/crates/bindings-csharp/BSATN.Runtime/bin/Release" /> - <!-- Official NuGet.org server --> - <add key="NuGet.org" value="https://api.nuget.org/v3/index.json" /> </packageSources> + <packageSourceMapping> + <!-- Ensure that SpacetimeDB.BSATN.Runtime is used from the local folder. --> + <!-- Otherwise we risk an outdated version being quietly pulled from NuGet for testing. --> + <packageSource key="Local SpacetimeDB.BSATN.Runtime"> + <package pattern="SpacetimeDB.BSATN.Runtime" /> + </packageSource> + <!-- Fallback for other packages (e.g. test deps). --> + <packageSource key="nuget.org"> + <package pattern="*" /> + </packageSource> + </packageSourceMapping> </configuration> EOF - name: Restore dependencies From 00d2741d9027b04d3b559baad4f5efacbf7dc5ec Mon Sep 17 00:00:00 2001 From: Ingvar Stepanyan <me@rreverser.com> Date: Thu, 3 Oct 2024 00:18:06 +0100 Subject: [PATCH 15/55] Reduce public API surface further (#145) ## Description of Changes *Describe what has been changed, any new features or bug fixes* ## API - [x] This is an API breaking change to the SDK *If the API is breaking, please state below what will break* ## Requires SpacetimeDB PRs *List any PRs here that are required for this SDK change to work* --- src/BSATNHelpers.cs | 18 ++---------------- src/ISpacetimeDBLogger.cs | 4 ++-- src/SpacetimeDBClient.cs | 1 + src/Stats.cs | 18 +++++++++--------- 4 files changed, 14 insertions(+), 27 deletions(-) diff --git a/src/BSATNHelpers.cs b/src/BSATNHelpers.cs index 46c4cb64..9c227e13 100644 --- a/src/BSATNHelpers.cs +++ b/src/BSATNHelpers.cs @@ -1,29 +1,15 @@ -using System; using SpacetimeDB.BSATN; using System.IO; -using SpacetimeDB.ClientApi; namespace SpacetimeDB { public static class BSATNHelpers { - public static T FromStream<T>(Stream stream) - where T : IStructuralReadWrite, new() - { - using var reader = new BinaryReader(stream); - return IStructuralReadWrite.Read<T>(reader); - } - public static T Decode<T>(byte[] bsatn) where T : IStructuralReadWrite, new() { using var stream = new MemoryStream(bsatn); - return FromStream<T>(stream); - } - - public static T Decode<T>(string json) - where T : IStructuralReadWrite, new() - { - throw new InvalidOperationException("JSON isn't supported at the moment"); + using var reader = new BinaryReader(stream); + return IStructuralReadWrite.Read<T>(reader); } } } diff --git a/src/ISpacetimeDBLogger.cs b/src/ISpacetimeDBLogger.cs index 5223a628..e8f01cec 100644 --- a/src/ISpacetimeDBLogger.cs +++ b/src/ISpacetimeDBLogger.cs @@ -2,7 +2,7 @@ namespace SpacetimeDB { - public interface ISpacetimeDBLogger + internal interface ISpacetimeDBLogger { void Debug(string message); void Trace(string message); @@ -15,7 +15,7 @@ public interface ISpacetimeDBLogger public static class Log { - public static ISpacetimeDBLogger Current = + internal static ISpacetimeDBLogger Current = #if UNITY_5_3_OR_NEWER new UnityDebugLogger(); diff --git a/src/SpacetimeDBClient.cs b/src/SpacetimeDBClient.cs index c01bb9b9..48b76086 100644 --- a/src/SpacetimeDBClient.cs +++ b/src/SpacetimeDBClient.cs @@ -783,6 +783,7 @@ private void OnMessageProcessComplete(PreProcessedMessage preProcessed) internal void OnMessageReceived(byte[] bytes, DateTime timestamp) => _messageQueue.Add(new UnprocessedMessage { bytes = bytes, timestamp = timestamp }); + // TODO: this should become [Obsolete] but for now is used by autogenerated code. public void InternalCallReducer<T>(T args) where T : IReducerArgs, new() { diff --git a/src/Stats.cs b/src/Stats.cs index 0d0ff3fc..bac011d0 100644 --- a/src/Stats.cs +++ b/src/Stats.cs @@ -12,7 +12,7 @@ public class NetworkRequestTracker private uint _nextRequestId; private readonly Dictionary<uint, (DateTime Start, string Metadata)> _requests = new(); - public uint StartTrackingRequest(string metadata = "") + internal uint StartTrackingRequest(string metadata = "") { // Record the start time of the request var newRequestId = ++_nextRequestId; @@ -20,7 +20,7 @@ public uint StartTrackingRequest(string metadata = "") return newRequestId; } - public bool FinishTrackingRequest(uint requestId) + internal bool FinishTrackingRequest(uint requestId) { if (!_requests.Remove(requestId, out var entry)) { @@ -40,12 +40,12 @@ public bool FinishTrackingRequest(uint requestId) return true; } - public void InsertRequest(TimeSpan duration, string metadata) + internal void InsertRequest(TimeSpan duration, string metadata) { _requestDurations.Enqueue((DateTime.UtcNow, duration, metadata)); } - public void InsertRequest(DateTime start, string metadata) + internal void InsertRequest(DateTime start, string metadata) { InsertRequest(DateTime.UtcNow - start, metadata); } @@ -69,10 +69,10 @@ public void InsertRequest(DateTime start, string metadata) public class Stats { - public NetworkRequestTracker ReducerRequestTracker = new(); - public NetworkRequestTracker OneOffRequestTracker = new(); - public NetworkRequestTracker SubscriptionRequestTracker = new(); - public NetworkRequestTracker AllReducersTracker = new(); - public NetworkRequestTracker ParseMessageTracker = new(); + public readonly NetworkRequestTracker ReducerRequestTracker = new(); + public readonly NetworkRequestTracker OneOffRequestTracker = new(); + public readonly NetworkRequestTracker SubscriptionRequestTracker = new(); + public readonly NetworkRequestTracker AllReducersTracker = new(); + public readonly NetworkRequestTracker ParseMessageTracker = new(); } } From f9c71c011a89f2a6f3bc0365acf3853ad5670da2 Mon Sep 17 00:00:00 2001 From: Ingvar Stepanyan <me@rreverser.com> Date: Thu, 3 Oct 2024 00:19:12 +0100 Subject: [PATCH 16/55] Don't actually try to connect to network in tests (#144) ## Description of Changes *Describe what has been changed, any new features or bug fixes* ## API - [ ] This is an API breaking change to the SDK *If the API is breaking, please state below what will break* ## Requires SpacetimeDB PRs *List any PRs here that are required for this SDK change to work* --- src/SpacetimeDBClient.cs | 31 ++++++++++++++++--------------- tests~/SnapshotTests.cs | 2 ++ 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/SpacetimeDBClient.cs b/src/SpacetimeDBClient.cs index 48b76086..e8d815c9 100644 --- a/src/SpacetimeDBClient.cs +++ b/src/SpacetimeDBClient.cs @@ -11,9 +11,6 @@ using SpacetimeDB.Internal; using SpacetimeDB.ClientApi; using Thread = System.Threading.Thread; -using System.Runtime.CompilerServices; - -[assembly: InternalsVisibleTo("SpacetimeDB.Tests")] namespace SpacetimeDB { @@ -180,6 +177,7 @@ struct PreProcessedMessage private readonly BlockingCollection<PreProcessedMessage> _preProcessedNetworkMessages = new(new ConcurrentQueue<PreProcessedMessage>()); + internal static bool IsTesting; internal bool HasPreProcessedMessage => _preProcessedNetworkMessages.Count > 0; private readonly CancellationTokenSource _preProcessCancellationTokenSource = new(); @@ -582,23 +580,26 @@ internal void Connect(string? token, string uri, string addressOrName) } Log.Info($"SpacetimeDBClient: Connecting to {uri} {addressOrName}"); - Task.Run(async () => + if (!IsTesting) { - try - { - await webSocket.Connect(token, uri, addressOrName, Address); - } - catch (Exception e) + Task.Run(async () => { - if (connectionClosed) + try { - Log.Info("Connection closed gracefully."); - return; + await webSocket.Connect(token, uri, addressOrName, Address); } + catch (Exception e) + { + if (connectionClosed) + { + Log.Info("Connection closed gracefully."); + return; + } - Log.Exception(e); - } - }); + Log.Exception(e); + } + }); + } } private void OnMessageProcessCompleteUpdate(IEventContext eventContext, List<DbOp> dbOps) diff --git a/tests~/SnapshotTests.cs b/tests~/SnapshotTests.cs index c429d07b..079644d1 100644 --- a/tests~/SnapshotTests.cs +++ b/tests~/SnapshotTests.cs @@ -268,6 +268,8 @@ public async Task VerifyAllTablesParsed() Log.Current = new TestLogger(events); + DbConnection.IsTesting = true; + var client = DbConnection.Builder() .WithUri("wss://spacetimedb.com") From f04e2fd102cee881e1df5d00b7044d6417dc1dc0 Mon Sep 17 00:00:00 2001 From: Zeke Foppa <196249+bfops@users.noreply.github.com> Date: Thu, 3 Oct 2024 05:03:29 -0700 Subject: [PATCH 17/55] Add script to generate `nuget.config` (#115) ## Description of Changes Adds a utility script to generate `nuget.config` given a path to the `SpacetimeDB` repo. ## API No ## Requires SpacetimeDB PRs None ## Testing - [x] CI - [x] Ran locally --------- Co-authored-by: Zeke Foppa <bfops@users.noreply.github.com> --- .github/workflows/dotnet.yml | 25 +++---------------------- tests~/README.md | 20 ++++++++++++++++++++ tools~/write-nuget-config.sh | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 22 deletions(-) create mode 100644 tests~/README.md create mode 100755 tools~/write-nuget-config.sh diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 69d6b8b1..643a1628 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -36,28 +36,9 @@ jobs: # available. Otherwise, `spacetimedb-csharp-sdk` will use the NuGet versions of the packages. # This means that (if version numbers match) we will test the local versions of the C# packages, even # if they're not pushed to NuGet. - # See https://learn.microsoft.com/en-us/nuget/reference/nuget-config-file for more info on the config file, - # and https://tldp.org/LDP/abs/html/here-docs.html for more info on this bash feature. - cat >nuget.config <<EOF - <?xml version="1.0" encoding="utf-8"?> - <configuration> - <packageSources> - <!-- Local NuGet repositories --> - <add key="Local SpacetimeDB.BSATN.Runtime" value="../SpacetimeDB/crates/bindings-csharp/BSATN.Runtime/bin/Release" /> - </packageSources> - <packageSourceMapping> - <!-- Ensure that SpacetimeDB.BSATN.Runtime is used from the local folder. --> - <!-- Otherwise we risk an outdated version being quietly pulled from NuGet for testing. --> - <packageSource key="Local SpacetimeDB.BSATN.Runtime"> - <package pattern="SpacetimeDB.BSATN.Runtime" /> - </packageSource> - <!-- Fallback for other packages (e.g. test deps). --> - <packageSource key="nuget.org"> - <package pattern="*" /> - </packageSource> - </packageSourceMapping> - </configuration> - EOF + # See https://learn.microsoft.com/en-us/nuget/reference/nuget-config-file for more info on the config file. + ./tools~/write-nuget-config.sh ../SpacetimeDB + - name: Restore dependencies working-directory: spacetimedb-csharp-sdk run: dotnet restore diff --git a/tests~/README.md b/tests~/README.md new file mode 100644 index 00000000..95f9a400 --- /dev/null +++ b/tests~/README.md @@ -0,0 +1,20 @@ +# Running tests +You can use `dotnet test` (either in this directory or in the project root directory) to run the tests. + +# Using a different SpacetimeDB version +To run tests using a local version of the `SpacetimeDB` repo, you can add a `nuget.config` file in the **root** of this repository. + +The `tools/write-nuget-config.sh` script can generate the `nuget.config`. It takes one parameter: the path to the root SpacetimeDB repository (relative or absolute). + +Then, you need to `dotnet pack` the `BSATN.Runtime` package in the `SpacetimeDB` repo. + +Lastly, before running `dotnet test`, you should `dotnet nuget locals all --clear` to clear out any cached packages. This ensures you're actually testing with the new package you just built. + +Example: +```bash +$ export SPACETIMEDB_REPO_PATH="../SpacetimeDB" +$ tools/write-nuget-config.sh "${SPACETIMEDB_REPO_PATH}" +$ ( cd "${SPACETIMEDB_REPO_PATH}"/crates/bindings-csharp/BSATN.Runtime && dotnet pack ) +$ dotnet nuget locals all --clear +$ dotnet test +``` diff --git a/tools~/write-nuget-config.sh b/tools~/write-nuget-config.sh new file mode 100755 index 00000000..1655fc96 --- /dev/null +++ b/tools~/write-nuget-config.sh @@ -0,0 +1,35 @@ +set -ueo pipefail + +SPACETIMEDB_REPO_PATH="$1" + +cd "$(dirname "$(readlink -f "$0")")" +cd .. + +# Write out the nuget config file to `nuget.config`. This causes the spacetimedb-csharp-sdk repository +# to be aware of the local versions of the `bindings-csharp` packages in SpacetimeDB, and use them if +# available. +# See https://learn.microsoft.com/en-us/nuget/reference/nuget-config-file for more info on the config file, +# and https://tldp.org/LDP/abs/html/here-docs.html for more info on this bash feature. +cat >nuget.config <<EOF +<?xml version="1.0" encoding="utf-8"?> +<configuration> + <packageSources> + <!-- Local NuGet repositories --> + <add key="Local SpacetimeDB.BSATN.Runtime" value="${SPACETIMEDB_REPO_PATH}/crates/bindings-csharp/BSATN.Runtime/bin/Release" /> + </packageSources> + <packageSourceMapping> + <!-- Ensure that SpacetimeDB.BSATN.Runtime is used from the local folder. --> + <!-- Otherwise we risk an outdated version being quietly pulled from NuGet for testing. --> + <packageSource key="Local SpacetimeDB.BSATN.Runtime"> + <package pattern="SpacetimeDB.BSATN.Runtime" /> + </packageSource> + <!-- Fallback for other packages (e.g. test deps). --> + <packageSource key="nuget.org"> + <package pattern="*" /> + </packageSource> + </packageSourceMapping> +</configuration> +EOF + +echo "Wrote nuget.config contents:" +cat nuget.config From 19e89795154a099c4a59fa8f384501aeddfb84a4 Mon Sep 17 00:00:00 2001 From: Zeke Foppa <196249+bfops@users.noreply.github.com> Date: Thu, 3 Oct 2024 09:48:54 -0700 Subject: [PATCH 18/55] Fix `gen-client-api` scripts for new CLI API (#151) ## Description of Changes The CLI arg changes in https://github.com/clockworklabs/SpacetimeDB/pull/1741 broke these scripts. This PR incorporates the param renames. ## API No changes to how things are used. ## Requires SpacetimeDB PRs I guess https://github.com/clockworklabs/SpacetimeDB/pull/1741 --------- Co-authored-by: Zeke Foppa <bfops@users.noreply.github.com> --- tools~/gen-client-api.bat | 4 ++-- tools~/gen-client-api.sh | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tools~/gen-client-api.bat b/tools~/gen-client-api.bat index 9eb66c49..23196f94 100644 --- a/tools~/gen-client-api.bat +++ b/tools~/gen-client-api.bat @@ -10,8 +10,8 @@ cd %CL_HOME%\SpacetimeDB\crates\client-api-messages cargo run --example get_ws_schema > %CL_HOME%/schema.json cd %CL_HOME%\SpacetimeDB\crates\cli -cargo run -- generate -l csharp -n SpacetimeDB.ClientApi ^ - --json-module %CL_HOME%\schema.json ^ +cargo run -- generate -l csharp --namespace SpacetimeDB.ClientApi ^ + --module-def %CL_HOME%\schema.json ^ -o %CL_HOME%\spacetimedb-csharp-sdk\src\SpacetimeDB\ClientApi cd %CL_HOME%\spacetimedb-csharp-sdk\src\SpacetimeDB\ClientApi diff --git a/tools~/gen-client-api.sh b/tools~/gen-client-api.sh index 91fbe3da..5f118176 100644 --- a/tools~/gen-client-api.sh +++ b/tools~/gen-client-api.sh @@ -5,8 +5,8 @@ cd $CL_HOME/SpacetimeDB/crates/client-api-messages cargo run --example get_ws_schema > $CL_HOME/schema.json cd $CL_HOME/SpacetimeDB/crates/cli -cargo run -- generate -l csharp -n SpacetimeDB.ClientApi \ - --json-module $CL_HOME/schema.json \ +cargo run -- generate -l csharp --namespace SpacetimeDB.ClientApi \ + --module-def $CL_HOME/schema.json \ -o $CL_HOME/spacetimedb-csharp-sdk/src/SpacetimeDB/ClientApi cd $CL_HOME/spacetimedb-csharp-sdk/src/SpacetimeDB/ClientApi From 8ce9b7b6ba0d45ceaec82448f78b27b148ad1535 Mon Sep 17 00:00:00 2001 From: Ingvar Stepanyan <me@rreverser.com> Date: Thu, 3 Oct 2024 18:13:59 +0100 Subject: [PATCH 19/55] Remove obsolete tools folder (#149) ## Description of Changes Not sure when or how this was added (it's not on master), but this folder shouldn't be here - we have `tools~` instead. ## API - [ ] This is an API breaking change to the SDK *If the API is breaking, please state below what will break* ## Requires SpacetimeDB PRs *List any PRs here that are required for this SDK change to work* --- tools/gen-client-api.bat | 20 -------------------- tools/gen-client-api.sh | 15 --------------- 2 files changed, 35 deletions(-) delete mode 100644 tools/gen-client-api.bat delete mode 100644 tools/gen-client-api.sh diff --git a/tools/gen-client-api.bat b/tools/gen-client-api.bat deleted file mode 100644 index 9eb66c49..00000000 --- a/tools/gen-client-api.bat +++ /dev/null @@ -1,20 +0,0 @@ -@echo off -setlocal - -if "%CL_HOME%"=="" ( - echo "Variable CL_HOME not set" - exit /b 1 -) - -cd %CL_HOME%\SpacetimeDB\crates\client-api-messages -cargo run --example get_ws_schema > %CL_HOME%/schema.json - -cd %CL_HOME%\SpacetimeDB\crates\cli -cargo run -- generate -l csharp -n SpacetimeDB.ClientApi ^ - --json-module %CL_HOME%\schema.json ^ - -o %CL_HOME%\spacetimedb-csharp-sdk\src\SpacetimeDB\ClientApi - -cd %CL_HOME%\spacetimedb-csharp-sdk\src\SpacetimeDB\ClientApi -del /q _Globals - -del %CL_HOME%\schema.json diff --git a/tools/gen-client-api.sh b/tools/gen-client-api.sh deleted file mode 100644 index 91fbe3da..00000000 --- a/tools/gen-client-api.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/sh -eu -: $CL_HOME - -cd $CL_HOME/SpacetimeDB/crates/client-api-messages -cargo run --example get_ws_schema > $CL_HOME/schema.json - -cd $CL_HOME/SpacetimeDB/crates/cli -cargo run -- generate -l csharp -n SpacetimeDB.ClientApi \ - --json-module $CL_HOME/schema.json \ - -o $CL_HOME/spacetimedb-csharp-sdk/src/SpacetimeDB/ClientApi - -cd $CL_HOME/spacetimedb-csharp-sdk/src/SpacetimeDB/ClientApi -rm -rf _Globals - -rm -f $CL_HOME/schema.json From ce76890ab6f6d103ff8834e5bbdede9d65a53270 Mon Sep 17 00:00:00 2001 From: Ingvar Stepanyan <me@rreverser.com> Date: Thu, 3 Oct 2024 18:17:06 +0100 Subject: [PATCH 20/55] Try to catch flaky bugs (#150) ## Description of Changes We have some flaky bug where Events are getting modified while we're already iterating over them in the snapshot. I think this might've been fixed by #144, but add extra checks just in case so that the exception is thrown in a concrete event that causes it, and not inside snapshot serialization. ## API - [ ] This is an API breaking change to the SDK *If the API is breaking, please state below what will break* ## Requires SpacetimeDB PRs *List any PRs here that are required for this SDK change to work* --- tests~/SnapshotTests.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests~/SnapshotTests.cs b/tests~/SnapshotTests.cs index 079644d1..8e99242e 100644 --- a/tests~/SnapshotTests.cs +++ b/tests~/SnapshotTests.cs @@ -12,10 +12,21 @@ public class SnapshotTests { class Events : List<KeyValuePair<string, object?>> { + private bool frozen; + public void Add(string name, object? value = null) { + if (frozen) + { + throw new InvalidOperationException("This is a bug. We have snapshotted the events and don't expect any more to arrive."); + } base.Add(new(name, value)); } + + public void Freeze() + { + frozen = true; + } } class EventsConverter : WriteOnlyJsonConverter<Events> @@ -344,6 +355,7 @@ public async Task VerifyAllTablesParsed() } // Verify dumped events and the final client state. + events.Freeze(); await Verify( new { From 62a092e19efc77e99231821cf3b8fd886827913a Mon Sep 17 00:00:00 2001 From: Jeremie Pelletier <jeremiep@gmail.com> Date: Thu, 3 Oct 2024 19:00:10 -0400 Subject: [PATCH 21/55] Jeremie/remove break (#154) ## Description of Changes *Describe what has been changed, any new features or bug fixes* ## API - [ ] This is an API breaking change to the SDK *If the API is breaking, please state below what will break* ## Requires SpacetimeDB PRs *List any PRs here that are required for this SDK change to work* --- src/SpacetimeDBClient.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/SpacetimeDBClient.cs b/src/SpacetimeDBClient.cs index e8d815c9..11237718 100644 --- a/src/SpacetimeDBClient.cs +++ b/src/SpacetimeDBClient.cs @@ -374,7 +374,6 @@ HashSet<byte[]> GetInsertHashSet(System.Type tableType, int tableSize) }; dbOps.Add(op); - break; } } } From 8f9614dcf625c1472cafd2621b478d6e92df373d Mon Sep 17 00:00:00 2001 From: Zeke Foppa <196249+bfops@users.noreply.github.com> Date: Thu, 3 Oct 2024 16:31:14 -0700 Subject: [PATCH 22/55] Update DLLs for 0.12.0 (#152) ## Description of Changes Updated the DLLs for 0.12.0 from the PR: ``` commit 0a7512d2a8db0dcff05aaee92f260e53d71cdc80 (HEAD -> release/v0.12.0-beta, origin/release/v0.12.0-beta) Author: Zeke Foppa <bfops@users.noreply.github.com> Date: Thu Oct 3 09:35:27 2024 -0700 [release/v0.12.0-beta]: Manually apply open PR #1707: c# client generate ``` ## API - [ ] This is an API breaking change to the SDK *If the API is breaking, please state below what will break* ## Requires SpacetimeDB PRs *List any PRs here that are required for this SDK change to work* --------- Co-authored-by: Zeke Foppa <bfops@users.noreply.github.com> Co-authored-by: John Detter <4099508+jdetter@users.noreply.github.com> --- .../dotnet/cs/SpacetimeDB.BSATN.Codegen.dll | Bin 47616 -> 0 bytes .../SpacetimeDB.BSATN.Runtime.dll | Bin 52736 -> 0 bytes .../{0.11.0.meta => 0.12.0.meta} | 0 .../{0.11.0 => 0.12.0}/analyzers.meta | 0 .../{0.11.0 => 0.12.0}/analyzers/dotnet.meta | 0 .../{0.11.0 => 0.12.0}/analyzers/dotnet/cs.meta | 0 .../dotnet/cs/SpacetimeDB.BSATN.Codegen.dll | Bin 0 -> 57856 bytes .../cs/SpacetimeDB.BSATN.Codegen.dll.meta | 0 .../{0.11.0 => 0.12.0}/lib.meta | 0 .../{0.11.0 => 0.12.0}/lib/netstandard2.1.meta | 0 .../SpacetimeDB.BSATN.Runtime.dll | Bin 0 -> 68096 bytes .../SpacetimeDB.BSATN.Runtime.dll.meta | 0 12 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 packages/spacetimedb.bsatn.runtime/0.11.0/analyzers/dotnet/cs/SpacetimeDB.BSATN.Codegen.dll delete mode 100644 packages/spacetimedb.bsatn.runtime/0.11.0/lib/netstandard2.1/SpacetimeDB.BSATN.Runtime.dll rename packages/spacetimedb.bsatn.runtime/{0.11.0.meta => 0.12.0.meta} (100%) rename packages/spacetimedb.bsatn.runtime/{0.11.0 => 0.12.0}/analyzers.meta (100%) rename packages/spacetimedb.bsatn.runtime/{0.11.0 => 0.12.0}/analyzers/dotnet.meta (100%) rename packages/spacetimedb.bsatn.runtime/{0.11.0 => 0.12.0}/analyzers/dotnet/cs.meta (100%) create mode 100644 packages/spacetimedb.bsatn.runtime/0.12.0/analyzers/dotnet/cs/SpacetimeDB.BSATN.Codegen.dll rename packages/spacetimedb.bsatn.runtime/{0.11.0 => 0.12.0}/analyzers/dotnet/cs/SpacetimeDB.BSATN.Codegen.dll.meta (100%) rename packages/spacetimedb.bsatn.runtime/{0.11.0 => 0.12.0}/lib.meta (100%) rename packages/spacetimedb.bsatn.runtime/{0.11.0 => 0.12.0}/lib/netstandard2.1.meta (100%) create mode 100644 packages/spacetimedb.bsatn.runtime/0.12.0/lib/netstandard2.1/SpacetimeDB.BSATN.Runtime.dll rename packages/spacetimedb.bsatn.runtime/{0.11.0 => 0.12.0}/lib/netstandard2.1/SpacetimeDB.BSATN.Runtime.dll.meta (100%) diff --git a/packages/spacetimedb.bsatn.runtime/0.11.0/analyzers/dotnet/cs/SpacetimeDB.BSATN.Codegen.dll b/packages/spacetimedb.bsatn.runtime/0.11.0/analyzers/dotnet/cs/SpacetimeDB.BSATN.Codegen.dll deleted file mode 100644 index d56f1f0f232ba24dd9c18f50389b15e95ce0f0f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 47616 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 diff --git a/packages/spacetimedb.bsatn.runtime/0.11.0/lib/netstandard2.1/SpacetimeDB.BSATN.Runtime.dll b/packages/spacetimedb.bsatn.runtime/0.11.0/lib/netstandard2.1/SpacetimeDB.BSATN.Runtime.dll deleted file mode 100644 index 0ef9930fd6d81bdeb798f061cb1c6288f05974c8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 52736 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 diff --git a/packages/spacetimedb.bsatn.runtime/0.11.0.meta b/packages/spacetimedb.bsatn.runtime/0.12.0.meta similarity index 100% rename from packages/spacetimedb.bsatn.runtime/0.11.0.meta rename to packages/spacetimedb.bsatn.runtime/0.12.0.meta diff --git a/packages/spacetimedb.bsatn.runtime/0.11.0/analyzers.meta b/packages/spacetimedb.bsatn.runtime/0.12.0/analyzers.meta similarity index 100% rename from packages/spacetimedb.bsatn.runtime/0.11.0/analyzers.meta rename to packages/spacetimedb.bsatn.runtime/0.12.0/analyzers.meta diff --git a/packages/spacetimedb.bsatn.runtime/0.11.0/analyzers/dotnet.meta b/packages/spacetimedb.bsatn.runtime/0.12.0/analyzers/dotnet.meta similarity index 100% rename from packages/spacetimedb.bsatn.runtime/0.11.0/analyzers/dotnet.meta rename to packages/spacetimedb.bsatn.runtime/0.12.0/analyzers/dotnet.meta diff --git a/packages/spacetimedb.bsatn.runtime/0.11.0/analyzers/dotnet/cs.meta b/packages/spacetimedb.bsatn.runtime/0.12.0/analyzers/dotnet/cs.meta similarity index 100% rename from packages/spacetimedb.bsatn.runtime/0.11.0/analyzers/dotnet/cs.meta rename to packages/spacetimedb.bsatn.runtime/0.12.0/analyzers/dotnet/cs.meta diff --git a/packages/spacetimedb.bsatn.runtime/0.12.0/analyzers/dotnet/cs/SpacetimeDB.BSATN.Codegen.dll b/packages/spacetimedb.bsatn.runtime/0.12.0/analyzers/dotnet/cs/SpacetimeDB.BSATN.Codegen.dll new file mode 100644 index 0000000000000000000000000000000000000000..222b592781a64e8c004ffecde831368d31b1689e GIT binary patch literal 57856 zcmb?^3t&{$we~va%uHq`?_@H0lL-MFk`Ueqf|x)6qsS|us3?SFfN01GXC^=l&`?lo z#nxASw*|rWVy(66wYH@Sg0}Xe^})4b)uQ;Qt@Y{^TfJBD|JK^)%uHTN|NoC>owe3p zd+)W^UVH7e_n8UjU35JeMC8KnmtPV+iYNcn3jAu&gW`;`UuDq4zGuoG)#g4^zIaV* zG}K|4tF5NBq2{Lcb~6@Q83|dP?V;B8Q2qSI&|0%4Qj?zUA7(ebU>4C_&7jG5tbf_* z?HCOWrD|h{nB!I4%l?69h;fZUmf*V5n;ERX{M4cxeEu<L#g#0||0kX{$t?Wjq22j{ zY4mx#5n(70(kKnsUrrD$PGWy88j7nfqF&HvCsnG6MK;Djcf16EG8wC_Hz@fD5v{1P zqE<5~r5ncP0r83y$3ZQotHz47nGj@M2|9}njxmw=sU@0`pcK}?cEu0trjwiQT7vob zp>1U;`PaY@JzPNKuXLW5Dq7FKt_6(_4;puEhH|dTGi<)mm*hE3Dur*lD%Tsq(eCg8 z4Bl1k(~VV|F;T8c-B8b=4l*Pdg|~Prhvcc}4Be@wE5_^uW0m5p)VsN@;ApQxR#mEJ zrTq+^bX8LB7=!X*1}L7biv4;x13guHJVyS~rG8K46-H+Y3|Jjd<wh4%()3BKGFUm? zW;>iAie@XNvc`lyG#VmvfId{?DS9}M3-xe19{wB(7qVqpRwtmMj|HVV5)ws_2p6jf z=ytP}&=p+91r?0V%+Ot%g+_gDXF8s+akkM3)9KX{jC=UWodwZpYL2E063zxy9RwSO zP;5qojq$0rMx!lQGdok0I%uR<Ww5!9uzOWzJ!<T_ss?0E2HAvW3C5y3^^$nuC(xAi zYtZ;X>XxFeg)zfEcE@%DTo8t}{gvbNupbRY;j^B~abX`Gx*}!>kyOfKG-x^cLX(&y z+dgWp(u^EUrs+a|mDfgXz0-4yxQwA>(jcSIG_Nty<?&8(hYzA_kJocyw2bv~mQP}t zz#1}{&J)w*s$|y}Bq4_(h@3pnkFPR}lBXWtCZ$ZWYY3gFjOZb3=LyGCod}Xx%d6E| zzI!twYw~Ip63yDr-l4!5)2TiaYQgzLEqNx`IX2sQ<~rI6;&hXc57_eZ9AGe5ZOMZX z5`&TF6>&>q-dzZ1$^@>vH^a4Dl?g*ju}g7LnMpRZNcC3<ekaB9OlXSLEo}R#?08vR z1*~$H9=r~1YMZgxle?E9m*S#ZCb@f&a=JesUK8dfT$@MoBOGFvt9h;h3ocx)+^?Gj z#J>bV6D%qWL2}2q=`^ebj)@k$2y5&DUOaj?7Nsj6F>x~^o*k<o6CTQqO1!B>9mJpP zC_3}|Jr=yZ!yE>DgvV-#<GRPZ1aj3rPg%$uE~RCgZN%i&Tlr3S1RjR9FkZ({8ko?^ zKf<uEQ0<0Ma3LB~1^JaVx)Da9Cy2;APd94P^=hwfm@C0TB+G}^yw1;up8ONW@cp_8 z(qm%a{?N!JOYXTS3@N`ZHZrS#F73r;1NCB8i!|HA9L3U=lXRn#t*v_H{vxV7f<nuU z>Wfs5hV&aoZ1h*pN$S&3JtN-wt4G^>?$nS;GKHTXPV{QXq?ltMFGC!nM_YKf_p7Pb zReQ=OR2#}MR6Uu<%GtJk7s6lIoC|qA{q!;PPLX&RLhE{lYx7t<-R3yJPRyK^?whg= zGCmVg+Mnync2#f4c9|1E`0<>GrxnFig(m^4Ugk4TXEArSyLv&k+nfxdVEpWCchH;y zv=f8Way{9e>hakg^9<(jGE7aWuE=)hD-rmTmgcR_@jSN{n7DIzDkQ6e)>ibRGz(7y zCweBp6`1;qF&tNlVe+sF^8<k}oVqG{7D_kQ0_a}1S%+sBsY#VNliBqEJbn+MJ5Euv zqqD&BdocdlKy<_9_MG(00UA9UAlv{@>B#$7{v2>b{#+oo{9K>|$opCTJV$<BLLP~0 zviyyDx9Ho=Q(%}2Sg@N_HB2lMe|Ql9oDh0v&<8FeEZc1B3mC=nY19#2g5meT*IZi| zSfjvu4%=_B*wC5}GTew3{DJ2db88Uy$FK;sg6ptGklI-na1EXiWp%6>|J}p$pW`qk z97oXe972U(wY`*^g5g=x^V~)K`Ojf?k~#}7W7#mG1K_2A`Pac?(7WgEuz~s60&}}g zdJbbO(G@6;HUactMcAYIch<4&Ul~=I{?(%@w1@9`4k5&^+K!Lvxn}SZrV{NF1_`&I zR0fXa->>SbQ>a?tVT1Rpx~6~CL2L#$aBTJr+pHBeqn82nAX?Zq`xRRN%U8{!HO%;0 zPpE;&sd>UF+UN5A1A49vyyvj96o;|Wl`9Tt-+`UQof!DDJn(k35j6pNuzKu)|Cxt@ zP-PE%q3df0-VBki8hF0rEQ3skC!y6JSgQ`!Y8`8ZfNE<s!i_!<IBl&qg8Z6=xt(i# z#lnnh^_+#S`nc;Lnx-T)eU3Fn5cfx8tSLOj*0jolei7JhO*er28cna`8mFS^AX<47 zT0PHNb+T6LSu0E<=Sjxq4WN+uY~)8c1c=NBd$<a7oGY<_BszkC!J@(tM4fdIXnW-` z?i458=#$5Eq4HPcF?{wx*ynS5ww=<H#+$;Hdj+cRsD^q@UNbhiv>T;zRvN_;oEWKG zN@Y2lK=;XVHiIQ8z)3r?*}`>30@xYMH-IV=m_0z9#0Fa@*gSsoo6MFBhOK5wiz;Wr z79~H(VdfasGjx+1Hw3C2W^_2wydicrw?U*jKb&aaP`zvi-^o?3WPD3JSK(<<v#4Zc zDl1pn#@fopLXN_w*~(@?lGlAYCf{(GSAl0<&8?lI^YN}~GZ)xi0MRins>cY@dB4TD zJbuGtUIX#e*GtnpTPCxtH~MXE1Y2@dj&?E+URC@$rFf(V^I8-|cQCjPpi;|Xb9l&~ zMWu+Q+9cil4)_M2`Kz!I^FI7EBtT|cvoNpL5&+qUt#1J1n0xBG?~`2PRF=+j--QyY z?WMqE_x%@kS$JyImIm13q~{1PvFp)B^ag+)Hc;gmnH0Vel$>v?kn_0cdu#^$WNP}B zn@}`-RQ`x9Hv>88L6JXt3-fxogb&J9TW(`IA#*#++#xbt63&E-zU8joLf>bhABYf_ zgkk!sEjxS5>|&X_MTSfG@}jEJLP(gxGwa%ngwmzEH&5k9I0OkqhLOo<B9@atAP6D@ z*ARvvU~pYw2!b&e<D7H>o)}m*QcaGpuJEI<n^oaOG=%$mbXWM-c!c3tp4Z3v-CIVZ z5yRz)?t!qG!=tr^Ls$>Pbv;%S*Biq6O9iSV<!AYnF`UM-pW&*<Fz=C`;E^7g1GGsn zTz2$zaQq%U{3DcaUWlHo2pTkyt^OG$(I0cex#%;z7g*&JRx9_sWdQ_bO6CJO>3Pi& z9p1wwNfkJr)uJzKWbreei6#a-<6RHAuZfL|&=&vLvGJyOQ&{eZ3-+nbuy!XGu$`QE z`HSR&YvKheLc&q|o&({LUvUmZSQ_9Q$nMbVz=y?+aLG<F0GnMyu_cPKhndprfagH& z(J&eIJ_mB2Mz{2gxlNn{xl==RE&P1@e8{~T;-7LpbYknRJlw=NklRzsLA9aQgsLYC zL*gtOp3i}7=Y?)P%w}DP#WLNASG<kh%l^X`RDy6EIIPLtdv^3EDDrz;o9|;Yg+qWI zee+N8h~5v7ii<)_j6XIDzMPf<>Ss(L)M}d_0OmD+4!EO^I}7kKe+Z;F=fglwdj1_S z`UvxSxP&iJt5QySu;~2JUos=em43wn9As1_tU!NsAM<*+gf9%M(o?#*Kl3qWk8+I& zRQb)vnNbR0y$HvWQ|lD$h@|Il-Vbh44f6nKzPYK&P03E_Wc72s+1~0vc8YlrMb)X< zUh@!;Tz6Hmr<-lDWgN_(+(aR2`@%)s2A_I!Y_ahW!vVM${A~A**)T+URf--I4ZP-4 z+$JBi9dUFGlCM&$uz#g!<k|dN$mn)Y7Vyo#OE4rnZGM^=K7@te1Bw0zKp4ZW;>c4K zeTK<r0T5ff^dlXMo#R`#G~3Npqa)BwnjZcG@Etgl6^F}EG2OT2c`%af{sPPXn0Y;1 zB9SE5rFXM2^J@#?G2U+EZ?59&R>OceWajy(14kykn>D_Bc!71AFbXA5WxFJ7-95a> z>SA{IMF^?T<Fmd2j&&1*9|A-<^s?_&rK=t!4kjtApY;4EYDZrJ;Nz=`h^jH`PuvU# z&TQ9~KSCgBL@#rtqs;5!62ALVRjuKEJ97IiS8AgEE4}OgDN&zGlIru2B=V<DhHt?z z8(<h~44N`ug=8N?Z2=#Ka<oLqPSz6JfTJasBzj?!ekxT9ovPRvlB*s|ROOQ7s{PGw zZBE1fAueJ45vb_zxBdn+)wX+iI(3x!<Ti77P>DkCB3~Fuln3fKtU?e)4pE$P_wW*j zC{_Nl17TOcdpNE+6JGZnZX^5#^i(tFh0H*Elq-9XJ?c$V{hB=rL5p(W|MnhL9dF7| zD?&2#Iu7$#LAD--H9c93o!Qy3mL(VPT5<|DCKv2S0bb9X<d-kK8{Uy5CYDIc(N6|> z4JP%k_P^t3q|XWAEwE&BP&qv|9{I743vaVJBLVE=!aG3KAkBAyI>!a*1hx#Ssvld1 zl+6B%Nb`Wq_mZUlD(rii{WoUIZpmAWDLN|s?zHvOjj1;RHKu$UP&ZjmL!jC<H-r<K z8WN_=_aRcf+}<5oRYNH(yQXv7%DNijy|QcKwl$%wAwkOgJGYY`PH1b$)@ObIzA_~H z+e}B#a0q<dxN@jeVOaI6BCzU91z**^1a-)>9n@9||8>zCyFDx%J23tT(~fmxsM8{E z(Y%yj5N@H(y;`_M)=R=I?j>7dy&_yJCbkCFk@XHUB!*&huZCUa8n1E$t}Da;1)0jB zhKtWTco8?yn))lM5199Th`L{~rjRs*Swybte}?A!xF~jYm7-y#f5dWpozCi+QiAH{ ze>3|G97J)oAO4t$eB>liqy`?&Nx~<X#qTiw2X@TG+C`S}33ynl7JfUnT#C>tLH!aS zCp{RgC;ATv4DaEREz5fsLt9VupHj>v;R?ijBRmYynvT}O!-0+H+r^6BZD1r$7hJNX zsdq6(<cUICXuu^LYtY4M+(nGL2m{-)vUeQ}+!Os@slz4Vkt}^TOIJ#5WJ7pcZl86= z>09a{WPT3$%Defjw$^Fwav&!?+1SyeUr1|Q62=m)!V6UEuiRiU*J4}ZUB>+yQGoLS z^CY+h<12OZOCTm*7WvCUHN|?+)PQD@^&zV2x;p+~pY&Jio%6BH`#qyZ=#nv|ri>b; zcP`-E2`S8!S!fxVjE>rAlXF3M<U|$z6|nTllflrX7tWSuA@8FYezy$rtK0kxJd5y) zNh5d>O~mhX{LaBIdY1YZ;t3NHEJ0P9tZBK_k?Woe*C*>Th4(Oiv*1-5tj846Zv9mu z5i1zrDpkv@8478%+7)t+b%#Rgtfv$*+4^sVjJAg4Nv#nn;r$pQ>X%pxz|z$)cPxZq zc>lHEWn{b4t0sAso&7br)-4eAdK2cYa$B!}m0p!?<pj}6(rXYOCa)NAY6gY@!)53D zU8Vsur&g7xm@c-rXB59CgHQWM1o2K}2eL~0QYyuCLtxY&U-zd}ds0jfh*x#A$LCU= zd^Bnj`O&QS2FESr-`Th&KqBfd8h^fFdV%;o;P`-GvkLq145tF52Gh}hqQn<Y1J@J& z4UFM4(}DZ)m!|s?0#bF{Gbxo|WuREjzR1Y<^BdX5s5HLKRaYumnqg)_f?tzW;SC?W z+rQ&{lzPk@pj+TwD(MLTIqAs*j0Tz4!zJNSsI}!XSs=M10w+DJQ8Zu5najIQRd_T& zDlAe2{yp4tnr;>Y&voUv!X-e`t0v=v0Hw@w8#(Uq5MX8*lW<^VVsJxXAl)y=Lq={6 z1%`SRc#L}7Te&4X3|P<{4v6=q$h`e!wdQGf^ze#?4N~Z0cstG_Sd)0W<u4hZZB6Ur z0ji{Ue4}oTgvge585|BsYV~8(o@gZshWBtucnroiD%-O9v|_z3j7MRP#7zYPz~dYM zN&!++rc`5{ri20g>Ate`3uk#SKWI*T53a`kNBsOLp1!i7Q?YB|IezMQ@SKcDOetk) zVZ5G_Q%+e!&FE}b*^-*GCm-P=D<dD_XcBrF^^M6-2&Xd6>74w4aL$f%E>E#8ixcgW zZ;;|0an9lt>;5=#K~-mpg||0$^9!n0rC7lNhis_2AjR4eCudZhZS4V-R%Kc*$aA%Y zPwjwSZcUQs1y-9p=UaEkbB^^pd7fqcL!PHwV~SXQbc)pm#H_)HtFx_5z*8jN+AlEG zb<+J{3pdRRZywFEZ60eMxYm9E-8=x3Y{Bxwwk96SQxwIgjxeFj4Uffy+ET3vsb43g zm@87@aqQfC(Vm*(@n{5}ftq1G#l5`%5S;+l6_fPhm3mom%L;v|(NtMuWfY@iB9}OO z4#xyt<{KwnoF8YSO~ahT?ct+Fr{fvlp3c#`&y`X{dB5DF#)~f1yXQ&K6fQagAUqx# zR@U&b)zfn}^p498!Ci(lYjm$h9jpzV_bT21vJDfi&OL|tV-4YHEax}u|7)Tza$^mB z8atDPYNFHe=s8OocD7T)>_%%l5GaZ(vG=>ewNh-DS8%_TD)$PUG*BC-H%|{XzJ!{u z#Lv4WFdC<IrjyZ45JsWHD6*z8LuF}>T~C(SS)tyIEyBgez>QL^z~BpQWQK)y4p`*0 zQ*7OpXs0Gx#{+tpHFesYZJ0B_G-m?rXy#E>8fHBR&d!FYW-8w-b<)uXq`!QFlhDTm zF9cp>C#1!|ORVP-JtiuCm{jreq>9T**bu5>8{=xo=NQ%lpbYB}V3=L8^1-<A-jq5@ zi4O%=N@SJB>pR24TLQ!zKaOno;b9`vNDLkR!G3~di;(BQPZ34nr?Vl$emVzF^K1Z| z$HEBnRbiwM?0z5cyk3$#ua!n8TvOv|SNkC{L&C*OcJu^f{T^d8EbH=g3q0xh1kC6; zXqf|ARpq33ZMM1Wr(7RlHJg)Dsox^?1)lUECG|wl<@%>{eazBa5b-IMak=Rmcn;xl zZQ^d#-5YD^_sD|a9r-*ST~!$4n+Gi2XX4`k>XIp7Y(eZsa^~|y7l6s1*}$uj@Iv6D z{*Y*)4JMOyH8#!If>`77@aHwEa@;5Lu&%XicU-GUdN-?YMDK<6a)Y=CZK1eZo=D#} z3xe@$e5Xxitb4c-LOdIbSy<*Pt8bYRUc!ZolrZvft`i8K!M3?5jE^tiHj-I85D<&a zr7Sx;nR8)34$=x0Lu%0%_ObY#cMu*n(Ag(^5!(2Qvk&{(0<;xETZT0u-U_a}?E#() zigvu0stycmcCy&T=s+$8R>7?OPW;PI%FZ()lZ@~sJf2I9PDIG!>U7<>hY`gM!r4@p zTe&+qD?JOn%Aps3o~Fugbk0Ht_^8bqkJr2uE%65(m*eT>&ml7<cN0nlbt|bZQQe}H z>{VWK1uAelbTWwTMO{#E8s#yYq@|UBomfFymR9v0d@8INbhHIvGe+m){Ix38=v)B- z`OHvx3O+OxL9rV5Dq#3z7ogL|i+YN=8gz6Gz-H_K_;|1QN-f5gJb#zQP1^f?F6XFU z_0GZ{lv8Ueq~c~qBI>EUJKTyc^={VNybP#)gL1iWSrfAj=u)1KI^@kVAB=Dw%g5cP zk{U2nGiy-+R}YCMWK(mQ?ck$EO*9Ktd$6e`HWt1+z-CD}G~ghGe9p54D^neHS@5JM z2fV0>7KZn5N%&0IIE?d9l|NJSSeL;rGIf>(OlH6zUdR2NV}va{M|q91ni`{Qd8L8R zZJSYUNbb(nw9DyQ(le+RLt#%RS9YQ>BGh`OFTx^Uhu-m4&f<rAa}*Db0nC-4AP5*- zE(}4y5J`>_f?%w?ruxqB?+2S-WZz#kzueY&Xp?^o>-BgmSS$so>0H$<PyX~F(*JnD ziR2nM0@wxWIu03r!fS?ss61)kPw`;oossP|@LKp1k9dF=OgwJ;1rwYJt5i7?BYaX| zXM#-(LM|geNqrb%U+1nX(Tkz_!hwbzPVu=LL#!?GXqDf=vlaN=5D%K)4e=NkR=$Gi zh(R9n*~t(2_~+*Jf^NWs^ZR^o1>MMKIE1cn>ol8{M$nP?&n?JQ3xcWUwuB)_F~*dF zb52D(4(nl@cKGA!n|OhRH=?KD*ohu1IK+<5Ry8ms#rdFX$6AhnXR*uI0$5{M@CsBL zVGHr{-GPNtyaj2TS~gukPI@Mzqi8oIhxc$v7)!S*JOjYo3})Ko-@(*ga|;V}e*-YR zy9Y4W<r{T|e6Yf0Ze`kCm4g%CHyQQd45<1BXB#-9-hMp;`(+SjD^{m$k2T2|2~L#a zfWu%b`$Mx}iR&e?M)*cm%Evog=619g99!bC&UTtm2L*YnAfq4QQO8t|>@!KW@XCSz zj8R{H`Q__=Z#Ck-ocTTGm1uK>N0&D?YAM*f#6++G!&mY6QmV@FD(q@Ty&h|UqeYHa zX)RWVfExm%UV1$(JEa=$(d^rWf#x;MV=aqMmq&I0yc-Y0f#{c1_A+I#v+Hy{y>gl6 zSNI;S<^?T{abDLq;t5*~bdN9nA-gyk<dB@Ss~T4Fb$yrvsuKgQa7Hu&NhQ0wQ*cXi z0Xx1^@NjYgF9TJef&-3peJ?`CfrVw~d}et&4+KBzu0D(lfL`ya-+^c7-Mn-LxU1&{ z)GL&H3ud>~z6Pjsv;S>ji_+KjV~fhdmMd9VyP~7Yb?;Q_;WN(3clDyD!Pgb`8*b;W zo;5X;zVAcz_+34#YRJ-8acWLDp{t<`y#uP@u0ElxeA>umUdQ}=o6p+HCx{eZ42e5p z99`}E?3s?f5{z&M&l;&PrutX0O!cXPnd(PHGu1YKItd><LuBMow3Xcl(T)8CZ_J~o z<JrRjB)$U<MdJG%sEHUmqwyS^xHQp%opk+#nlUxw$BZ9~#F@W^g6)}V5vwY2!+9T` z)d+kQjWMgWeKle;zEZZ5=m-+~iY1M7-&Xa#w~9GS8tPHD5A^lOAuDFI@rTgm2cc8D zpzQ8cAL`KmXyf^!j(h($e&^s9ag7k>2v=pYn4l)V_o5tB#JZ91NZ~9%b@)XQVO;Wb z;aP~^T>Q?!Z#I63?g+39XI&3vPAT%x)!7VxlEbhm<L`yJbYT&fzMXM(u7_^S`6%e2 zSy>EU%VyXj_*0pT9~XE?_&YP0vp=0-dIq=lc+RFg4>gxEY{+03&L}SS&`W8If1oog zD`R-B=&;nq_}X-amr329q;Cj7!wiPE7Ay_<>Hfm0Xkj5F{q&6><NvEa1Gp=fYt0KX zt_i#?pEVg`EJfW_!kG%_r|0vg0uJ{x+!tUt(ZlVexVg>e!ST>H@;BvW(D?FCLmA{6 z%J2i=etJ#h4*_P-6!1NCnzVU?F|{~{9yQwi@6z`RuxRMn5Vt%Yd_U~~?t$a5hL7j1 zhgFa1DIO0+WE78x<Yz-!=IT7|_hspKyTA~7@lz<9+qo8F_tVM(maIwLly{tZFui_y zc?fgbN<KzuP)6q#_(dA?n~fBYpLUip{=K0*#`oNezYF`kD=m1a-NpL+Q!Ms?%<uXB zk0G<D@MDbBD>~ePv1HJ>qSc*%9@>ePuhv)x4;_`dPs^;mVDLy^6!@gf%VU1#yi>wn zu>iF^^kE*))U#6S*F%|a!sdQTsbJkUix=H1wZ0=Vw+bGFwKM35^xh=62|S0k4{i7R z>4sv~aBiu$z(W`K>k4w{;o`aiKfNw~vJx%$X|IoMxHOevehI^K1nvYM-YE0lBV+s( zpr1Yvf0!>iKU=`o2!qe-=V>=Xi4(YRe<git@vJzt1*?fEUfEgHiPgZAA=FeU<L`WO z*?4&VER=Z23u-CmX9>Q`mm*XHG+qKZ4hLDZD*Ni(StuzN%9q9Ud^AF+-wNd?Y`Tzq zKb_0cs7k0T=`V|rL!#_Wk<6xX)B|c|*_5J75EsTvnOk&msM~DG2~t*NQxk>y)Rvqi z)Y8mij6Iu97wU4Gnk>}TELAo|s9#|Hv*Z~<ofImEaGHcJ0jU?DT2;@c5YtdrCsZE4 z%>{LvP(i{z2WpN`1$2&3r`goGLj6FfLOwY_^2gcSdl7%{7}Vk%Md8#B>N(LRjPDw2 z_}cP_f*bNKfi;&1wa#FwidKou4i++1gI7{4xlPI@VozXdl$4!Ln`kTA>?~%<DYR9} zejZfRHqsD)clvI~%hsmR?I_J6ue5R&DBdl0L2?8>5xZCFtxe~8_4J6ed7-pfkME0b zLk~|N)?7lf=zvh4;6jV3Cwi3~Bm<HkqGc{SBouNxP_yYNp=Jv;hki#!c@`N6RhQs< z$-hsc8t4U~o`pkS0!#mAQrUd^leB{S50ovW*OST?(J?7oEWKYu?+KMHtt_LzB}raF zf0r`&Gg@h;6GDA1ZMM)q$d57n7z=0#zOnVGQ0uWZFTtFoXl#iiSWcJV?R=(|7{lc@ z^+)N?q=3e)SW<5t<!d^qCvdTN30_x@NUGOGle7_ZqUeE~5wtG%VZguS{UWEFM&~j- z;r$ihJ`cmcdS1l6#D(4$edW}a!tl|Q$G|TXd?KJmXL_H=IlpIV>5IPed-jyD%r5Vf zkooVFCjhUK7LG~_|0$erNog{!T2`D?pK9T0w0Y$aZu5Tyt{3>2z{>?*lh38~g0~E1 z{C&Zn7WjECa~=(FYdO-pA=*|H@)%z%JgSw`y5I&ty?BT|mZlawlQW5Sro908tD<)S zW2K)17NmM|C(#+cEWqW(MY-j)$MdmKPN#W;?sB@PU|8;0I-{@#d~Uy-?iSdN9zC8{ z+`qsmMwP{&$+o<hZv?GRdlqn7+KYgiz(;&B)cTgU%sZC8H{>kro$G>Ab%r+y{JB1f z9&y*B_eb0f0ylYrbQv`Ro~FMEnBgi;3DVuJt`wG;mm8#Y+DyNPUN3yn$DD)SAWfy8 z1AbzR^9AWTn&}VHSl4#ICk0M&Gk!jxhq8+!UhK8GD*&zZO<r!H5p7~m0X(Ly0z7Ik zewC{mu*iKqbp9Z;(}v|#;aibePV0;AflX?Peg+wT+Ajcqlh5$}vL|E|e*nHhKLYqu zdJXVu{jb75m=>fPv|pr^)AR5g$Ey2e2Cq$d7Mw+}@Cf>_^po5Xw6*9nz?#y}q^=eS z(h>3m7KE_K=&FKU_?VwYOM6i(;?x-Ai5uh8L{L}9DK5JuPPx&_Jxpob=2Vm&l(Ma$ z+}PU=3w4Y4A7ym`7ag&w^L<Rc%9M5sQD`=(zuQ#IH#^{_FGcby@3T2CV1Fzqha_F) zZTH>k!}tG~(ypSqvbo6b=1AF7w5ZrfO`%mny`wEEUKsGwdb`Z;e*ydD7NKtOo?kXQ z;G=KZWe?@<!wfzw)K<FG$J9}wu7cJV1pM@dO`Y#!>ODnae9J(6E|ls$jm{p*dUEeA zfplahOg)7w@)wZ1@3$%LFN3}m>K2+f<g!2prQsAobPJ6Jl}Uw)@?IBM7s!$m29NfS zx$8mQDrIW^a_EO~DhsEA`xWJ#=--FD{}G#-na9*)Hg#@Z7O3AQ)eF!|c3DZ<KD-xv zJzmxm$fFZ>nJ@jjT0VVlQ+reQ;pE}s1Bm!i7N}fBc@LtjfQoIZI&iD6fGTWiYT)WX z0aYi}E2R23H5&VKqb+$y>5f1#T^ujd@D`}K7j=0rD%y+c?nQ0uMP1vAx~Ug+XPmkk zz2B=S?}?mSeI@i@yetcEj}F*nwVr)=7xc7E-93b<=WXhNA@~vsy%Mj-e{JH;ICXVk z2z_KrzKkBq=(BiP7T&L94|l9PnnDy(ly^tTjzBr_hntk;ZvZtXPTd-)pi68?ZgV(A zlIjhowQ)&}h7*5R*qOiKwB43uFBm~P;$>F{M$nyh8Mk>F?T(ject7`(IF$wJsW|n6 zK$u>&>y?)8!*?d$vZ*^t?+H}W`*BGP`PGRyl?TetsQ?}F`OChmjiR7XS9xb*<&2^s zHg#_9y@645noV5+udAWaHnlDF-arjaRuo-|IT}rELOn&i{zlWILLJf`FA2Fv(|-$f zi}%>jp9jX!Nt-$XY8(YlV_noLo<Qquilfa0+O8<9_g@Dl(kC{h(G!75^o3B{5hs2J zY9!Kg*qSDm{ULBVO;Z%gR_Ie`+DIt_^~b=O)E1}eTy=C;oO;lhMTZqdk=*aN8|Z{k zHx&H1Fi$&&KB#e8ae2-m7gB!5f^#WLsH?oKo<e;t71&hF^J!o%mD^Nf?gaOFG}5M) z<x<`}njn<2*?j5|O4)2ay(|>l>}>Y}ddsGcfLcVK+Vwc{EvCtQK>-W0%@)(yih|89 zaxbA>Hl<NU-ubjwD7IO*`vQ7eD7INa-cmYlQw5=L-bM6Jp?H_kXc?uA<sNRKf96ig zTSjehYF^%@v`Hvsr4_VroRWOIXjR?{T47UEv?XsPX*de0^>ABWgoX&Ed~gj_sxmM8 z*%}&WQ|xDJXsS(Z!pdo-nKpH0+P#5Rnrl<L^Y<Y$S!`1e<lh^(j8-TLWB6g-<#Z%Y z-Iv!+ZwYk^<rF=V*Fj}Ci1GMdE_yQ0q6I>!S&7nJLaAAa(*Gz)%*yk5G4kNZhI-y3 zpw?4~P|A}x(iWTIS=mTC6opy&Q(hN+VN)8tnRf-banvK?S@{d7u|n~z{8wH#ohy{G z(iXZvl_5%v_iv%)Hg#J`mj4^v1bz;=&nvh;qleb0GPL=b|C>~c10y^{+g_ZSxt&hn zh{*oJD|I{7;>{=O$x7W$|05K8-EaI?k_X2}HNM~Yuci{A)cCHU`)rEGcMa`R6vp=l zKi{qK&r>)GeU^7EwcvG|8lN|H2R$s*Rys#F{nrt{g+uRNjzr&~2}1Gi`<eecG+mX^ zc?CxBdOBMurS*+;vt7pV<wp9xP^@)k@Fv=8Q%693kA7p<<F#@tJ!w;{^{w=xQ14*n z6b5gjS5+CtFg$oW{mquts5*EDeJm8u(dnQ*SM{_>xzmGpl3t4;*g4-_<QM7|%1oOT zyo>T}s&L5M;P<K4rW*4u4*rl9*whpGD}p;IZ3f}z26`=Tb#NE8+0;X28-jc2m{7OS z+l4*BAJd_kA}MicFTEwyO;n^^8{A9h&gQaPXlj0*b}ucnsq@or4BktZ*;EnA?xXcW zsZ8fS>Je%i-K)*f@1tEwW%twGq_X>IUsBlvbSSCp0eV#^6$>7uza&XMNFUmg59zlB zAEZxh>TTD3!H3jUEN!Fp?)|}sDKv-2u#JA~emeN?RAE!Ua=#FKghtrZbK0MRzo3yq zss0|Ni9)FnJxXWVl6&;Gf{)UyB*|aW!X(LG(uKBUl&<vrl2+N&82!JnuXWf|zN;qX zG1_QTKXGkK*-zVqQoSFb?+T@QKR~zIlHJ-o{{eb7sq6`Q$u4^YWls=)ivja4vw4t) z3Z+^(NR>iu!`;GR??IYoQ(qcq`VP??o4S?e`JbfuHZ|3Co&PENmQDQu)Nkp4P1U-u z^Z$;H*whM8Pm|Wby{rCyPd=fx(FxZ}zTZ=6QrUkH|Gf{Ml_&Io??32bp_JB#X?2q1 zVKQyWTsjdvOq*=#J$$d@S^B9>y=j~XK1cg(>btJY{1@o&HdWyc<sYF>gi@NlM0^Fs zy<h3B&VPxn6-t%;k#4ffZgEe{|0C^+mwn`ZneJ7Tw|(fl?xXaeO>G=HJ^vNjXH$oB zU%)-f%Qh9wyI=bgoe+vo8iz7pB_FQ5IAU@Pd5uCsy`ya|EKPfj#tHR?c4^@^QC4qD za%_E#t`}-6ag=(E-WQ6a)PtF?)2B9d1k^D?+8{k}TzZp=gyKEm-2CGdRup2-1^I80 zOJ3EhIQTX#5K8s%HZ=;xJzSdq4lTE-BcT35t#&;g!(Zt}o8tcdO7{xIqg|8#H+oel zHMj56UxZS&exE)RY8!6M%>4K19RAn^W~Ei{%Kv~av8n6zE&2aN{P{)4uRo-#g;K42 zNZ++3@6ZmXeMq;)%Wlj2i0)UE_b5(^C+HEIdfRtz-~>HxQ=BDyLcg^s&JsSM=WOb; z(tWtodfBFYL+%aygWeKK+3cS*W<G1CZ1zvOMkuz~mHD62_iXA2sQ;lK3Z>@#bNaJQ zvCTfGPlaNeeK-FLI&T5Dsdnv?v|3S^v0L&r?FT|_l|9bT?p0;pOVfUkZ)gwN)S9$C zpnh#rA7Xqi?VwG4f$_Pt!$PSsxHWHMVhnEWM?&!!ewy#m9<-?=puF1egi>ShYfBe% zJ(Vl^wKYQV7=DqTs%0+Wva4|8keZpM6$r%{;A8n|+G@LOV%d}V>Dp$Yl!s($*Qhc` z;<Ibo4MJ_hUF9G0Gc{Z!(3sq;ES)y_6y6u&+XN!R@Ta(QWqF4F5Wm^}Zc;5(%6v`u ztS#fJg*tztZcQ5Fh3VYFqp6I)C{VS0xQsdP;7y@MDbm+M596xzFL?c@(Z!<4;S$D~ zquU&XE57PQ$uAtrk_>fe+0ku2+SjR0^il0Ac#pJyE;PYs<3yiioIk$nSfe7K1D~I( z$J?2Tx;jn4hz$B6#^}P09n1g!;$J3X*$A8Hv^qXJ&WQds{heBB6dOG}3f1O|h1|aC z>s!ztc2IMTd35j(LJsb<a|AP@<Ia!A%U0F#PLu8MWIl6}Ev#^!M;*6`EYl`qIS0My zHvWOkmzqrl|J8Y^OXCr(P1kK596eR<z50@|C>>6<7pJuf@rUo`@fej&`j6#Q+f4YC z)0#%pq~#HKH|^NASvYJrEMf74voT)=Jr5a2tApabr;E%#-Mq$?yn;^~+#3HmQ%9cX zdN5qGJwf@y4(a_FSv71!l;T~2l5twNU3#A`YbV(z$^37~x>j~muugnHLB*LZz4V8w z-+zR-U;I%xj3v1*g%8GO3pny>rWE{Mk_D89Tq<i#MYgY-L5>65g3<@~!lxWVlzpBO zOHLP?oOE+LYsJp*$%^3BWKjALX+OT|oi%l8eX)k``BbFh6`@+=w@D}wJ14KuQ)`p; zQPE?)tTk@O@d13&4K1I{P2qRExL2;g&F=`@)Q-eWU==<OT8&$=Q8Zku#objct;KU3 zKFKi&`SclhLpL3F*1TtB1G@2P4#x2YsnAbj0kiOSkzt*{27wC&UMR3hV5`7&0=oe7 zkXLX!w*wZ_F2N4~j;5o6zXv#x>U5TQ9x#iJ>YI@{UL&O=4bB_K3Y=n`Ib=WmQgY5; z%2}N`FX6<#tMKpOOAGW*|0guXFv`B5kA(9P?eO_DhM8J};R@slUa0M$EoG+x`u$_I zn+3mF@Vf-ZsTKU^>B!JoC|y*1o_3eOJ@TIFLgUV%+q99|uYK2OO~$)v-v#6hY>IJY z=*{5YQF;f?lkb<kK=%=6&kfqMIS-??V`z<adsJIz@JM&*MHP=}pFz*3wLS74>_S5; zeNC&=-gN&(dzi9P3-vwnPHdlyWuLr{+9zkyL*QJ5QohkUETxC#J=I|;JtCYV;QU-a zV$09>|3-h67`AG+r9Z3JX@!L!>aE6Y>7VIajpO*fa-H_8vQ*<O>Tv~(8;s}t1>n^C zhJh1sk1@7te9vE}Ra8s>e_dHE;P(P^0pD~lGu{_{P5?e=d@k~PjfV=}L}^y&FW^X< z8go9U!^X$PHL%G)4VT8FNCT{M-LCB@S?LOBY&X_ty{kk@he>I*;1d8JG@3NF_B7$t z3umt2ivjCg%QSu~xJ=`>g3II<XPL(Lkjpf_aa^YHjo~t6cozZkElW&ePl#ze<C_HE zBzO!m=Yhj;8CvdkZ`b%9bcYu4{Lp>CxTfG9_stsH^DdPBopxx~`rmc80sqK-AE3)~ zp|&ZQ<GD-Yu|EvXMfyI#75Y&v=wISFr16`>L)s(mO@MsXJ}fd%8xIV95FCD^cu3<@ z_YvX0r=1aUrySDw9pfR5&)=`w@*~RTgTrqf4{3Z7f6L}?NWU@VEuN8-z1pU<XHuRw zx<f}(-j~v&#)z`Nrc5!8=cRd1X#CFebMW!^(KUYG__@aK9$m63k7`wU>w)td$dUSv zlC9pO#<8^Tc>_AX$qeZHRx_aUiM#~z7wJc#|KA|b=i*_)3F!Q`GobT1z1o)9kUr1X zrR`5^_wCY*{C3|7jo-fR(tcgq<$J^UcG-56K9F`RINvJ0({~h{`+OtyVWB4=$!~5? zX#D2(gvM`fKiBwtK22IaV0?4vN4|RCAGuFxd=q#Ael^~I)F=zF&iuwTpz|F8M~@}o zZ}(jc8NPj+YirBzV=+416)d(n{BAa&^X<Vhoo7Fw^LyHW&Nm1Fo!{05biO~})%_#1 z#^>7;@U-6m=NrHi;%O~9ds>Ump4KLKo8U3QV}iHn>~C!XV*=-520!!9l{52PJNog7 zYc3+vAHd&L=uLf3TVGI^x=FO%E^wi~Begd5Y2(kS>r;0K=VpOT+P>Vwsdow9r5y}@ zoZ1EZKa<j>eU|na@K=Igrf$_|q(76g2XVMG?LPfjS)u-hK0kD;?<3<|>4@a|O}^T+ z=V=k%(A}>8tY~)H!#dv>?9xB-T?CvX-Y$JaZgbl0x{7;4O7BcNYWPy`^X=82DV?3R z)yOLSx3qmazgynN{sYJ--$SC+VSz^tSHZ`qb<@yL`p@<G{&DGh^?xg!oPGpaElqz_ zw0cY5Gqg~Do<bGf>2IO0Z>B%3H$khX(fieqVVzs`MMJJjZ`IFAy$R4$au?vZl6%s- zw7Yzdq&I21iuR?yFFn3t+&A?3^eG09Ws335lD`4}DEGtkUD|ixhbL^A5oKSdABE?X zXR!aAkx}mAJ&s`(eGWZ;qc4<I?=l!q6WFA^UEG@Smc+e)#`d{O;}{sgE}&7s;0Tbd zkM>-b5kM~*jda}*yg6g6YfaG|8BN+y4|>=57JG`$@pGZp62e~Ndbs4b8N;v=U(KjS zI~VB_1kNSC(+=P+W=!TZgXiuBEtoqq^K*#<(+svrz0RKtsMq-ps9wffZ?INx80Q4K zGuP?I3hv5maQXdrXD%c5fqI?&xn5?uUg!IndU)oc%#r$0AM3Lg`i#`CE&6BXVuM%5 zh1!3XE%7WiI36y8Rv)=F?2n~cEmGPd{5Ijo42ivl#9qNS2_7>f_6poC<6>(s%fhF5 z{CV`e@nu0<)?R%|X}fO+N;TR68y-w6G=^jLmx&GAWL!s$>(lpSeMIM``Lvs*9gcgy z%eu?paqSWN+-LAy+-Go<UIy<Pn*Ffw9~N6J(|JGQ7RF~UlUn<v^pI$KNVGjH_+h~h ziOz=w-iIFlj(p5V_vd8LWLlq{fzO{^3)n&T0><cRz>V|{;3hI4(?bP-+i4Www`daJ z4q6C!JuL&gnYsXPqgw#)qVMN;aUZk?@Kf5GlY{lQC+9TebK6U6C>5L<ymy~mP=oi_ zb$}JL0<el&0mswN0jJWFfHUa@z;o$Uz(%^bu!b(CR={TZ0pR8IGr%Z4TR0x?$}@^4 z02T&jQa9S4iTrYJP9r#Ffo9<}3nwC+sK9N)-zNNRxEFh@=t|+^b{?(W3|JW0B{I8h z$ZhV1<l9B}h~z^ec|b}J+K^=qLME;FNs)O`WZsa{<H&aR<{YPM0pFs_^uoZ~BBN=n zv!QXRp>b)h#+sK4ze1~oJ{7{L(6-U|;tG)&t_`K(f$_qx6Umv7gaw2%6Os+ZGexpd zI87qcEHcf)X@*QkakI$8z$pxD6`5@!vrS~SLFTIBZ6dQ>W1V-2%x;m{Ei${MuiYYZ zk8mCW%qTht{!fb!3jayrzbNpywwsO=9~b_|!Xcf<sOe${eH*=2Z0KwUmwpqSRhp}_ zH3EQzfpU?lfXv6m6(Un7oSEPhmCV$+<(a~7fP6+#qrfH`a{JAY98=OPGEwl$0zK#* zW0!Wep>%f1HYwdH{9VG|E&ScW-!Gg40uO@!TJb^Q9}~_S0*`}VRB~MSnjzydxEI6V z9u0%%(J*+7A@DPb%0;FE{ADE-!mks~OmHwR;nW*<LcYNmN`ay#8_I~J<z{KQ8S++1 zv&gqf3q4Z0&4%3jHj&vTGP|Y6-GUz!nS;{KLGW)cIVk+6!6^(pEv+2~ALA0f;o3&~ zOAMFHl#6xqx!5Nw@TJB=K<=voGVhjDh|EYAYdaI1jMACHSs|Qek#83H9^q^g&Nks} zmzH-5f4A^=3xBV04uS(u6VB7p`!V4k7s=zoe@{4?n|n9hsjR1)^$ZE8TwsM;bau1O z!`wWUVUP(F)k<l-;0-7(3^a>mvq<(x=~k4M6>Ss#HfedM@OQg^f_C-`eo#seO09#U z!_&fl!+jG~6}^F4Gm13NO|$@z@sLMsC9qlGHi5eY?iP3elEVWB1V1S7xIiuCCh9EG zQijqsMMes@Q!cPVV4c930#^uZ64)$oo51a;TUNAF@ZAFUr?8#(3w}`GG2t8&{J22F zTgkrdl{n^oh$fa+2&Y0gwO$#Kz$Pzi-XwUlz-<C|3fv{7yHU3=u-nTsvRhg>0RAmS z2Zeu7_-_d3xYQyax3BqFJ|uX#;7z`vbazn;;P60?@b?J5U+_bMzaf&EpX<8(L+Sj| za>0iQUhC(U>x5G;oE5@p5>AV7whF#W;2z=Z7nuXXIRs8q=`rEFA)L2_L#d*3s^~0u zx!}W6S=(CS)Cs3aV2^ON3cgeD{i!l9f*+7t$AoiCIFu&sr*UhcG?okrr&jP<!Rw`T zh2Sl~Zz<{#e5b%&Qo2j<Hw4$xS=(}fbpo3NZWXvQoo8)lI`_LvIQs=ZAowxC-w>QK zM7InXyWr&lYcsfoTEXiCt`N=&!J7p32;3@gr@&nT_X|8GkTSXbkihax?xj}ny3CvC zfucI$tPswjOt#Nk0z+Bcdnk*eM6JM{EN*9~z&%;)8~X)6B=|AG-x8d%S<;owG9kfh z1=b6v1+b}fyTD_@ryS8RN9INFTES}tUm>tZI6Z>z6nv-P`vo2oNV(E)?oetg4GCT= zaD~7<;13TR6PyClqrh5$D+Ko7GiVt_J%Vo!@V>cI@cjah38Xw;-GtA%V2pVz6B4{f z;CA8c6nsA<5i0~gB>ZE-IVKzmN((`00pA2hJHn|ISdY(yR29_+hth`96;irF_$}Zs zC~6UYk8pMhbmeobbLF$&hVpsVh5>IX?Gb#xKq`<?7yMq^o3lb-PZ9U)DrU<J6Id^B zyTF~rJknzVYfBhkA+V){<$DC*DR95QV*;s^C2Ixt2s{P|Um7ASLf|ohp)%H|R$xyV zmmU*HA(0PBslXKidj#$jxL@EgNMhlX%eVyA3S1$uN8nC@`vtyL&ORR+Dq|72Um#UT z3j%8et`OKGaHqii6<lk-;Ku|%CO8d~x&mtjt{5im3*IAer@&(ZX}HJ>tQELI;7)=2 zhl|e$eoXLVg3}18HA4JE@LIuZ1z#bsN8nC@bQ<gMFg=R*rqAH5zDrx8jnud5Kh*!E zk8nTYzAxp)l(4td|5JZ%>Yr1+X`|Cx)9z27k#R-FFEifF_$(tkvo-U_nSaPUH>)eF zE;|MH*<O6}$R{_}eoDoEY54o>>G-=J`S^xP0q&!VaF!{?OTZF*UU&$&W#HnQ4>;8n z<9>Sx?zhWvwi%B5?bC3t9maij74EZZa4qf%y^c3wJwApbbB_aBzIOp#p}z^A;SGTg z0jKys0i2)5FqFnHJ^eGlpQbYYTY+Z<xbCZ^CjmdrVZ7eMr4J8b_^NOYm2hb?u3A%^ zbB2m!seyMD8D-r5{9NYLNso%N(c?ww>fH3Wj4EB5&XP~&GgOj?N($mK-y1p<_;DHI zhP-Ov@A(ofr;CQF*7~$@DAmv-pG^&%Q2nsK4vVJ%W`L*Tdtj-6`QYiWb|zp2=0V4I zKyxu)!vXnk<>Ud5gp`i=3Wb2xm=7Iq7)l{i1E}N8K^fpw%#TiIq7DAn=!T>8EI|HC zbf*E%LK`~11ylvtfZF`8C5#4~huS(V!g*E4*>ob{`S{+Kj=!}&1uZNE)agRBt<gn* zc<YEBbZST28kvCjCr$9SLZfwnI!@Iy0k1@F8s6#80)92V@v7mRJqP$T=vkw01L}BJ zaW3E;_$HS|cLM78cGP^p&+v^S4S!8?A@Ki&Mml|tZw2X8pj`ldA)t;=om~jL7*MAY z?PB1ifUuQz31C=T4$eqGSWIgIUIkc-mo}qep)f`<6Fr@eZwW=9^%c<VdYtt4;61`S zRHmJ-P1j8A9?h?h*XQfk>U;G=xS6}!_<`}F5p<1mt#w`Ry36&D>v7jht~Xtux!mpy z_gME??udJ>`zrS>?gQ?_?zh~ZyT^HEc`o+c;rYySX38}w$5QgWOTF#h9p1g(SG=XZ zdfzX7ulv0IPXCSmANpVMANRlSFH5aXy(abk)E86p(oRcTnD(=@BWdrZHKeahUzh&d z^iR^y&S=cIG~>#Qdoo_k_<P2e8E0fhGxe<0tUy*pR%O=Mth2J_XRXV6B<qW;)a=6S z>g>tcbF$}WyLzxdvDOnm_u&1To*?Ps>}T*Uj^F6x<*EJCg72p?+MCBKMqbSFLw?p~ zx8p4(!IM&G3}VYz{EmbFj>qo={7!@iPJ;iP4!ci=2TsB78SufW_??E|GvSHT@p~42 z`F&X(erMo!CVuPjI}4oI_?-jDKWEdn5I^VA%XpVan8g?5*<$lo+0Qlhv)z`9*>dac z=SKV4W0!x+etu@x`@*Kbw4a8i`g7aQF8jF|&&~9tb|szE9;bHWary$k-*i1r_u%(; z`2CwJTYJF$DgDv?ITd&wr&7;JJV$H)={X(zv$VA-bF>1_r?f9+q2~5RwB24)OZ6?( zM*6z7HNGpg8+@PAk9^kx|CIRmcPM+$ce`fzcWGz(_u_Y<wgta;_&?GjsV=P}^<(^g zq<tUwzXAWZ)U&kT;Q3VQ>G&<z?@n8&{W)!{o}WHmuSvf^e>=TdFUpAM7=sqexJh5* zE61;nHgweelo~QWrHe9us(&vtTl)yVPihOb3$otO*JOQ+IvYWMroRRJzq2@=8CXXK zo+<eC0{7zS!_$YSA5VOr88I&if1HIFScI5Ygx#me-hF2Alun!3X0}JB52I<*rz~GS zX89PJ*3jM>Yi(+4y&^JwrOle&ynK0mYqX=SscU9iQ#2}aTw&~3u2It*Gc6WYWM65U z%i=7(DAHkCvHI4g)$L|9*4iAEnp`3+j$>@GIcsAq(jIL!+f}h}xqQ58Vs&dY7O@)5 zPOCXGzcbd+8521vm1ZWWX4KymkRF87SGNI~6S?l3NNjOeN2H;Bl{tO&^5se7Y}1<8 zv^LVh#hXO7Z>y8IfxgVKrwf<OFj;lb9<iEYk^0CgHAdmG@`5aAvZ9eXDG@quWYJAk zoe^z{wa;#iw6%yFN@|*&xi}rO;mj(KzO%|@{bqFlCMM5rMvy@!4<Z9hnO$)_x3DNO z=#<%F6Im?D%sgqrON?m}izG-4G1>Xlh{mqq_As=`KpJMXcdmt%SGGljTG!bWvx!+7 z@u%>k>|rRG(G-pFbk;|j+nO*bin}P%)H1)lt!t*)))r~DnGNmDR%C6YJ=WBAep6d# zB)Y&d*SEGrET@CE));E6?P#)Wo>Boz1QN3>%e3kv(Ppc)LvduD7e%6-Z84NLMo<q< z(bCw39yZQ}n{zEk?>ftB>f(;puI-e8IE*#m#Ug9R(vpVu*!XeI!~^L&@pc*J6<FM< z6e%l*`GZWeQ+FK83^3v2Xm)3N^NMjKmRa1{0kh-d7|L|x=-fz`6f9_JwZLC9PIR^# z9kYCVe`Uc^Isq9(Cr4x)*B?M9aFaboNIP7OSL%*WR(HJUWH&ly`Go#Dfh9VP<H7+t zOq5R?upHcEg+TV6qJE4?y({3mPt);@r{p$3o@cgnwne5Rx0^P7KE5%uo_%xqa+=$; zc4bQw)y41!R#tW*+F$`t6DUEVD<R%Yja`VQYikmI9<7-ZL5OK>rqz+y@`h+*3~Qp9 z`c8qACL(!53$?_<I*57r$HMWbYiSWd@$?poqD32puSX*-G%dM_>6b5GKBKAma{M_M ziEK1Y3HRsEYH#V!gpX}7tpN(o!(wk*9qC_#V|jn>d6C!}vt{72*^#DLrxh8bu(Pdg z;1Y}jU18fAxVW*|>=-~14om&jo6*_YhL|&O;i3rCYnP?gzh+#ELCP05t?plmC#XNS zAv(W<qheEA|FR|408Ca_Hq?G|d|qVj%7_*1-%g#i8WFr5+k3w{Gm#v`tftlh3(-IJ z>9tL<{`KI@)<A5Dxc$W?#P(yuOsgX;yuBzdUB$aFt>kGDXNPmrVpF9bvn_Ki(`qd_ z)GC|1z>0K4P+Hd^4;DJN)s|+13BgE<47jO17B5y#N^mOpDz!&q(O6S^OOvI#vX_D^ zI0v6;!FMqSEZ7|3rKxf#!p3`cQ*_Ntvn3*WE^e}*$vh_2RwQV<Q*5s$bS<lj7B#i6 zj!;AUdh>ET61y%naNcM}dD9&z%)~b`7%PD<>THj-u8oLg&&G&Q*Jh~^_vW!}Yz{^t zmDt7!ma5d7$CZ*ejR-gBh4<}NJQK6q(B9VC9+9mqX`7%}YYbu+w8qxV!C#_kk`*8A zXljl$#=20@*tK@0*=DEA14{5Ta~zP_5d`iUkJ8*Qv~Fraw3b2^SHlo;Z9F>3WF-Y8 zT9pV+qTgam1=Dylv+V#SJ;#Y@W2P@(uGC&^#%1HdLO31C0gM{gAdMG(JC8%T?_3y* zt+<k(th7Wm=}KX2cZG2&Q&K1ha~h)d&Lf;9?a|H-J}#(i+g?F38}S-W?nW!^B+i~E zZhHQzxR^@qFwJpJV@ISJ2c0;b6jRkgR`FFjjT=Iw5kR18yyC1jNeG3W6Nx30JR~HD z9T6+mrK+-87p#d`5mNKX>XTX|@bbudL_t_kmMI~$MXV094NI10wX`B_nPHh5WHIrM z45LS*2%K$Qj<3WWh-M+4wzNcA5**vNE!$ZZMb>q;S_28l1aJ<Ts5@(8b6aPNEEtXu zGfk+8%`_np$CmIW8>gr~va)maYF_;b-aI&9Z%&`FB}AR^E#~ExVAq?G=sYu~_PK^t zi30cw@1iY>B1udY%4YNCi`3xqgfxx-$Z=FIpWvR?)U`6gIwn}?ix~~8Y$Q>l_N{*G zy0$iRL+>^)#ZCRW@y_59?SnMiyC{AzPDndenvKwPKKut;a{of!cX>aQ{kosjOz0M| zoXs|IHcGT(`%84L*({ADN}TSoJ*{e8-KmaG{fnJbPcQ9y&19mE<DAO82|musO*km^ zWhW9>dkrUQ;5g(gp%%3pt9>C6Ye}Wfi6lI!$X*Ibkr_*LRb$htNUUoS^6hA%BMD0O z(&wm{ROFZrJN^3B$Od}>CJCseDP~<1X=~ajSW<Z-cJ|&TNh);Ak1*5H*&It~<P^*_ zJG!ja)oYT98ZU3{;E8OD@~{(C2Dg@-h9yLuRg77WA<s_~C@ZU^A;FaZDydY0*MLE( z6Lx}^P#P!b_O3*ModB?@V4jt&e9TUi&geok<X|gVtzb!6Fj<L=xOX^L;_;0wQJat4 zZBa$T{0dpVo=;UsbDSG8`~fZQ6zPK6#qeaB(`8`@ve=YI3o~c80occ_#cL4dTd*TJ z410(*_UhykmBY$WU)eRDR06T{3%h_jo*QXj9b3c6-27FmaL}r2Z?Rczszh?%D5n>+ z#A+pT<z+Mrd#n{{>|C{~wYe3U_N<MO=1vY1kZr!4Ot?-fQhXU$BIB3gC6SHTP9jJt z#cs}k66-QrGk@h}xI@8Oi?k(**LL9cvEAnK01!BdV-YoSGQ~+?{fwF!jdhFX;dI&( zSsiJwX+i1IwQba(vK4BOjL#u>lXY-jhH^`n%#bW?zMPyKwzy?-MaJVhzAeStshz}T zL?y=)v*55H_TcAvO_xU&ZHTu(F{dpOFH>g3Y1=jeHFmDW6aYjRBoEsja4%jjQ@~QN zI5tzBt;NW^+tDar8!236WapTz?PPnDeH)Mn?{*Ht7VpL6!&nj{Zi^(Ia~Mft&5LXh zQzo&Nv^QB@Np#F~_78T_INoM&Dr)s4F5!snm~<%zeOa|+4$q4di|X3jO}?ox+a>iK zfNd{51abRDPkNkh`#83jR$kkvkHu+XH0tn)8|L||<cNbrC?aQGB_Ls;ztr-1kx0ut zvwdD<^}Noti8?qR;bOgIKGF`+GEs6)6n%;#^FD1)i=g6nu!0bLcfuwSV%k`w?66{- zp^PJC@fj0J=E}n6U|Oow&Pav0sI`O6Yi+j7sJSX8tE>*oy(`)pt(n=lrpfB)duchx zGCMnHwLsgRNc-0z?QdU?JXdLflOfNP?T}(8=b}_ht{gSaQVFlUdy|T24z*I<b&wfD z+N{=33xY=Ls#cIPPiIHkU{ahrIcO(czp9>MJaVKZZT*THq7CtuV6oUt47aHn2?NFL zN3A$^<2VW)M{#un#(gmhc9IKSEdCDEgn8p<U&aTxR!ce><BBb2AC#qH(#3K-?I5|J z1>~s{1=KqeCzk@V&y6y*#4nd=eJd{RWGwt9!BK~YVZV9lccB->s_ge_!M<OJ5})%V ze8$^yb}Z*?(HODTw>C$jcHWO2j01jD=HdJ%tCiFKsDwm014iQ)?@C1ZDc|VeHB)C4 zqOehXlXsfoOLqyGyp18Yv|-;;8+24%dnQJ~?^bv&qY;+nkw?ji3cM9ZX)VIF+GMee zqqq&7uzY!JO>2CFN!IGMHU?QTa#i0Jr6sXeKwg>RNoveC%1y?zcnBrE>w;VLe!Ji- zkR<u|HHjT-o%dwU{PJ3?NrYTwH7C->$Cb1-^DU5CwCXyKWs<HhdKa;5YgBff_K4YO z-vIWlGN43!9ZC*owIbx%(~f1QFx6L7Hd6=hllZbrI7JKaN5kX9S_wGOq$}vNO}-<I zI(J7ZN<@k8xg2~o0;)_4-^+@-^`?>{Cwy)rt{d4Mm9lZily{;q8gIR`5kJr)4hb^c zXyWEwB@fDT7n|mJ$Zlmx;QCV5NAdx}?!dYGlyb>toV~`b0{`mGNzyk_&=hY6#c&t9 z!^CP+22Z93d|ih{hdTj8XM{>TA&;XV))U^awKl6oB%1Q2QrAKb#R$<YcEOy^RtK%e z<-%gV-;)EWT)LUj)|f*p?@MYgewXVcS#}vmPaEatkUO>8WG@F!65+RbHpxmPk?aVG z{h=Kb-5ev8v*M*3CktdBun^mt<>nc?3Ol39w5ct!Dn@e5BC}(8!&()Vs3FQrmv;(2 zHQ<$qN|A)%KJ61N_I@iGn~%&!4!Ah5@C!i+An|Konl=s_K3)}?ZR>FbBX_tl5?yd9 zOwKIJ6u^ry&w|eEv=TUPWx|z7B)$PvSd=H#*~0C80r_QANyh^jFB5(#35ovib8(Vh zAK{liPAFQ;IFB-pNFpE<EGN&!7)gxfl0sj6|BIirBwcbO)s?k99Z1$3e!N<IWvVsl zlsDTwtsf+K+xbq7s-?^%i<)@t*<5iFbud($l+EvDBJ>zluO@oQB%-oii7)>o>r2XL zlZv=o*>L+8JG*KB66Zd&e+jRbfw?N6_Al$(i)jrsT6PGVA652hXm5#Z#PuaZcnV;1 zY@--dHZ57l3QM~7cNpr)ZZpGd>5^J_eL;2YUG_@k(AocF%#qi$x_I?D3t3iTd&JHP zIUlkw4)9JISxTHrZ1RnK0gDV0_op3za8kIL+}`3(CtQkug`ftXlOlX7dJJu*Zo*$@ z#wVvH0H=|&$ijane(UjBw<x}EX5nuccHld4%$p-+ouDZtgqk6I7;`Z-(Q064GL>5m zI1e101RY-bOw@{i7Xzg3S@>pM2e4+LTOieh&jyCjOS^C^l$!Wk>Pnu=k#~p|?b1&Z zXs#0ynYH+*fm?)HiCQhx1^!CvM2!%nO?j$uV#KL4@ae!hyjRAbS*mD6KTXgg0-09P zw_ZL+mLyZY1Ug00T8HRlK{A3qLU?w8gVCh$C^q8HhbL<BNY<jX4drYF9u<!T_Y^YD zRd`xxLutj*Jf3!2S1#=Y)q*);nXx=8trtV@fwf(1YjO#sxvx(2$?Y|x&-Qq$Ye20> zIm@v1+t6z>e%VUvQPzffJloWByJIZXbqIgt(lIzs(pObyb;m*75N$v$?qCh@-oxXr z+AzcoXo(HUlO7smaLEJ0<o#b5p-CK%9fhZahwDtAiC)LyqlT04WCO61v%z9)1fOF_ z>iL;7OO6>Fv-GVr(4g1KOtaNvVn!Y+W6bGmh{2Qz;c`y-%fKGc$4KkcYFup()EP+s zL55(EP7IbkV5Ml(0@w^wDWmb^Y!H*FNo*G*wu@v8HI;wESlKI4BMQFbiPUqzF<J{$ zXSYhYcft`JLnx0*4hL$$Ytbw#H}J4)&>e>zHPBg@Id=SM_{ivDe84k#KF6Ys^;kw6 zm^kd!2-a6>o=DBZM@^@5Xk3`wda{WJUs&wPY)<wf4k|oxt$4P<&taz21VugfI0jOS zBN6`wYk-wcUQ~`_J5hsUk!8EM<K#-AR&>wG3~WaB$AlS^2MQyc>&$3KVvhw$j%VDy zC6n4$i!LeUc-=Q*68;9QikJ&za=0ZJWy9C?;Ml2ePyKuNx=`ZuALZ%IS6Z;wF2XOj z)`oFWZi3JLpV-mrB|OA;xy(km8SfSi*hv;+*BscoH7IxXw04ZUjc8V1&fqiCjK#xq z$1biyud_4LC3V8#8CJ8QTIcz5WJ0LjjQ=`?qrzT`P+W^g(z|`N+i*UB55G?xY&?lw zj>pEfYl4m!pbr&X@yB%y{K;7ZAMkcp<CD8oaUNuNk7ms{qi`ZjVn?LRKKX*=5DCwa zSt#Ze;PBXn93<=+svAt(<VBna5xv7h6bqiRJ7feL)7hTLV-|)H3V1^c!6JVAH;}DW z&<WvF=uVtvE3Sm6b1Qz-#=qKR|3_{=+ctMOEsskp{U|k2|I{p>7)zoiM$yo}?V9); zg)u@kTRamywx}K7)i&Kr*PhoBiZ6%gnJ|*tx;Qo50Gu}hel}vT9JF|nmAkVMdB1JJ z7ba9#N=!AUejFP)omA;i7=QeVr!!H@2}#L`2~R%n7b;w;AgLxQ3N9y19A%x2jMFDI zSRPsvCWKS58uT<06M{*o>OJ&uQVOIdJu!MZZ!qWTy%ZXup9MWQ9poj)Ht4@RsH+2| ztPf{LJVH|}6^hHw2JP$_;eOVgk5SBk1TS|BmUnvKDGlMb_XPKz(rr9+_T<4-NUY+2 zO^HN3R$>FxP_aOHSuZ6n{Qqgpgl{A2<W9%d-`fB*Q8^W7wlzsHs`u!6>x(}szU__P z267tZ<h>0xcQ!-zV4h{?c%tS$g!yrf7$HgFPi@G~OeTyu4dH{s@pMdCcdt;QQc#{s zC%E+1*vZ13HveD!NUi>Ed_iY3zJxP)zX@+njN<G2PRRCkA71s{ax6`b1!{#RyOm>w zZshaq;tsvpc6f*GZKYG0mpaTZZ@(+yT1}Ym=J*yA!cmx0p}{41<O3hs5+1@4)7jK` zwpODLj&9tWv!Z+Rc=S9UiBUSEV?XUJs}_jE;~3%e#lAI3d@GFY$cgWXnEv&84?gZ1 zw0%0-4r9GId&H@<$GN;3gW7~=!S;y*^u_q<9ecw-^X^!n8($Yoo_~kWelQaMvF|cY zr_F$UlaDNeBqrI7m~m5fF+0^LWuI)6J%=v?`Xnxrq|BDxsI70ABsJ$^e)(u`w^qol zB_~LHQj&D1s<*GQt{iJr8pl>*y)0=7|CBruE~G4}%Vk^V6od=}vNC732N~V?!Ti&y zSBDX*ix+jFg6ocnUstDw5-GHkrqHN=r54TkdTCyJ>|MN0I1k{wf}eZ>Q+Wjr7x?E# zF2EsXu-1}SA+NnvlCL;<i4%QQ4B|@|Bv^xvBe|^^m>Isl8vIa@EZqQ~JvGTCELn9b zqiMit7yqipDakJR7i1f-1K{6DImLEQUA6(*PB`UuPhEEQzo74IrNh`$v9HSYcqOmK zfo`xX!l=XzzGUN-%lWH10CN=MeLaK-sdn;_L^DsN-#Yj^dwdt}IaC|Hyz}%^X=4Uz z^GZpyMI#3rL0^q&;!Jq9lWRLy4;+o6;_>Q&v0tsroj8yf`B&Ww@x|_`U3c=@?-QR! zpNb6}eULU5_dmbHup-9sS}W~cw!}Wn+ok1$^~9@B>VJiVy6mGtC3sbSbsI}i&bOB| ztGAXsLwv)k?$exDSR>hrikO_m)Zoj|D$|&ToB)w+_5WQfd@aH!9@URJlW>dYqW`X> zdp32wjDCjq?q{C8S2|}jTbomgvvE70l62gngYICxqq4F;uXKDJ<}3ES<XTQ2@mi;( zS%Wi^rz9Pp_fwK~W|^i8=B>&44>X&nb|G=T#5g`^@tNQ82f{U3^6dbP9W2sBljM>k zC8hIu*Evs-ffb8uCC+scM?0E24?f?HKL)`5y;?GYR-CN)8b@8wsLLmHK3)fpYef|0 zOsa;obY3YO$N6@F@>RylnX9_$G?5h`yGj`;zD!fqI{NCYpWAkFZk5M#?nxEuoX9at zo&8ykFV7O^O3GE)pQ8n5tdyR(;=#pbHcRkbjFN#R$`Z%mz6W5|no{{i5a*AOU9N6A z_+-Jm7q`KO^fQu969Zi#B~EDJUdMZ$iC)@esvO(NyJ=?fvAciWWF0s+ck1zJi*0i` zkKeXQ!V6#p{2|Rnw)OgsnJv~!7WVln{mCs>E}exx>(Plm`8r(P7bWeG*$Ldv&+~cg zbD@LdJ2pQxablnp{$riULoXPOKXnnpp525V_%-S%_y^Za<ga`p7%iu|3h;S{<<~Gu zkC4bZ6raA;-mm34d3sWxc`EX5KriZaAg76P)|Ph$^@f7Il5NlLx@^7i56vl$kH*ie ze2Rdr-Mnk_1t~{&_Fa|D@atbWX{2%{qee)j3A}%MB&Q>#h04|cPkZMd8^>|o@!h@S z{UW)%Tapgt-kmRrlPFE3BlRPYf;hGzyN(LWs%_bcff|NHQ8ppjQbjv0Q@Gw0sy~cM zsr^Tg8gLC5V1PIY3#x$r=pRJmphhD!0z#lft0D^8q6QM6c2gh@YM_q$`M%k`+aqPm z{i}b}N8ax2%$qlF-n{qb&D_jvu6!F;Z5);LsJlt@J0D-P-wqw>_j~EzdFkt0#(sL+ z{=fL?FR#A#<3G7e!y&)e@%_pWm&T-m;ZP^=L#{;ZFHJ@d`HiN#brjcfDZf`%N)(?P z)_pRH|I8ov3@p91y%b_8gZLg06(GMywxu+21+GPjkR?9J4z$55z{g$oMp87$Ml#`Y zBvXa5U*TJY^pG=u!Z0kAgGQ&(ES8~^Sl%^UAU3R&i<L%`|I%b*E~r$>)N9-u_c^X9 znr|d88(34>qD5*JEvjbGVu6ENd<!*2)++#Wf{TU5X=J}*6(l~Sy4)%pITS?m(LDGA z@bfzBax@=C@$*sqjVOMB8vi*pemRQ21-|$>Rg;`?0&o<c6$9dz?fOEI@H};*QoIu< z%P5{11`3CR)UavwZkFnt&C}*L1V0?>RB5v?Y$U5>ZiR(X^mHG^)ycXafaO`gt-_4i zQG9i{riK$8t!8M+y+<e+_i7oLt!A|>&>y;Hv3su$jrf%^e2dSLA<b{q3jF3&r&A>5 z+yL?@KHmp(cz=}Q)xC!|ikDsLkJD7$bEy|fu!E>-kLVFjAdW9aPg8uWIYhI@SHsCh z{8z+Yh~m{KezoESMAAAII3Ko}6@(#qwUNALG2d!N^M%s5&&g3ya;cGAMocWcrUpzd zkrwoy;};aBo#$I5kE!%;2rhm>&y4||6LWPc<@X>>@Z$!n!p7(e<3vB_4@O^)qZhIo zzZS)pqWCg`MFZ7TpDUtG)sv};l8xk~88(vBi(VceCyl-yIm;5lM*J_d#idFXFb<06 zaCbE238Ydo`j0$Xcv)iq9QiKyL1={@2;mDE$XXv{JqJR#2eRG=fe$?_gnI>Dk$TFE zRWTTtAY=)R3!=pu!k<Q<D^??yOpRQCbH$`z%ADS6<V=Ikii~@sW-hWU0Ayy<nFS1{ z)aar!l0Q$22V)I22i4C}-l3YJYh@(uMXB<uPUGyUCM$>N6FUM^dk;a|t6C$xE?g?L z5i96w+xAAyEM{bxAv#;EQP@OJTr<2dE`pLI=wqkAF`^`<8I++|7W$unWlBcm?E)8Z zrxCxCTAzy*CH#cAi4L5?{8O_&)JOWc+WA}`tHbF={GWbX_24Gfyj|$SwEdBq<GdJj zafDKmp`ejed8krV7edf_v6It&&`0K?$-9y6aWCx9{CyG|8%S(>t@k0W?GN|pqD6#V zmN$@yxh79&sbGt&Mq066{Vc@{+N;%00hVF+g1Q<xZ`_RUs|NOOCC{fX7<IOh?l!zp zq>0P$&2FlQ@V5O@4Y64Yu@NDL8jw(JXT7ykgmJH17N&ovWNyFU7A2Dg8pZ2Td<8mQ zr}+r-kDXl;0znr`h^95P&?#1#jnE59TXV{^hz)*9%4C7jx->(iQq1EL`DBZ-{*0^* z$2VeJ@kz_DD`~Xjaz#mt<T@F!Bz?G>-iraQD8B+1k}YJeNDJ2qq{awz@w%ldkoJj$ z0{R~qx1RBM-L+lDT%+HJpjAz8*R9@b#Zn{wwlj-j+Uf|^MT*73RKZv2;)6cKy5(L= z;T(S1Qe(N{ykZbnz?>9=QivUqYq6dghjm!5Hs(&JB&ktRDT`5cwT$z*MmqjYUjV=y zu*vdJ7)HsI<8-`a_t0#KrCXs(J%%iWFa)<}X$aYCh2z%!rChKZwF4lNb<}C7hUACV zfC;jP^BJ1x*+Y#O8ap|QpC!p`HOI;Y>Id^u`ck7S$%4<irAAjlx(0kR;L||-?GcMf z^O_}y-#|JkHE5>BSQ*iuM)GOc%9SkG7P<(`R-o?h?Ls4d!)zGVtdVTHdD<_)PMIP3 zI6c@LXqy+FI71ISEaMt&((!N;@+e-(@)0yvuVh86KmqL{nT%HmC^eZf?kBtZmMxhY z#}iF%LCp{|TJm;G!kt#Lx9obSiTY|Id26IxkagHkJ<M;n_EJkL&J@?u>g6JiZgQ_l zP$N0Or~x*;lZmQgdP(v)?v3UF!4a&?h?0j|O$mDPuq5no05t~;Jol7FN6B2N<Dth7 zS=}CT59hH0NgWzfFEC2ZQ-Fk|E+Wxeh&!K3z0xH0x=WL3y5vAK-~4#JKxd>1ruHyZ z1htX+kv0jHS_&*vgoo|m<!Cn%;byB@Pdn5e<?^~-?^nwy$-_`ZLcYG$UJjazS|aoq zbK?<CJ@#(5+9cWCIOAu5p)61>OCiHbL0msGnsGf!9!O14@<1T1Q~%)<+zh-KpX34L zyik#_hnc}z9q1GW@}!-@YB;q?i&I{dy`sh&q0|Nc66UEl%LUPzEEYGcM=S##`7meA zr*eLwY`gpbI6?|OmlvJc;xgbjVvwni1#0SlF=J@$C_}#Z6{{%NhcW%4pJ13-#tXG+ zxlJa?)+hc~YR}^rrHC3G{C6)#Dkh(Js$Hg6x=bU@kbcP(EI*omywz>4U1uPcVjPU} z#jo`{<J2*>T3$vtMHeomTu2LHYVE$I9-wmyn<wKM?94}bqCeI!QsQvt6SP>RSt~n8 ziZ!E`+Glq&qvwJjEu#!Lij0=+j?Pm8Y_g8^=_pAci$r6ti&H(+>pm@C+Hxw!6O=W? zryWL2v0}rV4-15B%`1|6QW%n_#=B4`@+!8%FuLRX2^*poik=@7oCc43zgezEi&$q6 z><0nEbI|DvvS;ZAES^XGtIHbbRh{Fd3|Y?KxK`&bZLYNa(Hh<#cwHGXs%cPlQE%uH z^pqndi*_ykti;oihQdaR>Qwd{kJMXeiUK}Fizz>>)2jUF569oVyzT0}m6^ZZ^WgI5 z?%7>gx%H7}8tb*sPiRi=nCE^SbE{z%ytX3qldgk5cegbkq{Z?lnay#_hJMTDuH5S1 z<{eD;XtL-a(^Xo0bIrs<<kZ5{eBwm*BhNtOUCga%Wm-P6^4k=xsj(aNYlQG<0n_XN zOii`Wq}?BQ&8)O*l9VO7>2~%)=PFZrz%ZA-LLn!-qQ|G`bMd`GsWlXo_^%X7%=MHa zUWgSC$$UqvDTnghgyePOpm{Jd)lhT6ZGhPet@T3dz0eg4*>J3$;m$@nl$h*Rv45}2 zKkD+&y8LofkUej}ir?ruY>vbTjb-`LE?&Ok8xW9cB<gLt*7LbmLyd2GeH&#bBdI@C zDCCAd$yAnwJ_5Ea%23F)QjTUi)X6K<kPo*#u6$~~7wFCd*2o{C3-nuD1HTf`I1M^3 zCf?MyR5XnkFcU`y)zoxbqe-fwIt)l`jt=-vRpweVa+MM=%*F_9Kqk#C2J}#h4qA4@ z?^8mP&H>Lf<X~@{o^$*Yp^%whJz%fTsmOX*58r=Q0*i>R05%&lWd5!V59u&$m^vmz z<6hm^P0HxBhKLd@IY=L|P7y$jFwYXrKttp4f%xTX#?zer_>E&dQiQV+zg1$CN9Q`i zjXurBR4>Y9b)8>`Kg{5c7F4a5hOxd`*zsqHSjV3w<BuK$sllW8Q#UW(`OL!|h$q83 zRdBgi5JY-Z4WezT)?r#vr_qlA-HNb^Sjtz=>!qV;x=~^=_c-vDv^rMz3(+DItVSkQ z*$bNHjWM;s5EKt4ns=aU%xoeT;~Ilqpzq4f-C5jn8kg#@Ik3z0*cO0MYgu4DOS&>9 zTxTV4+?X7JkA&pX*ER9r9C_!~XA#uRMTj}cls&!!Liz5DXN3?Zy_k)btUrfiKJsE= zV|VhD*r^dqQWOih*{Fk=h-3<hFBFWicy9)#uFio?nC2u!lmYL$NO8!ha!dB1#$0`c zTrpF^S|`08GC`5`TT<p?1vO;RQ-yC(+{%MuIy{9uMe|%*3ZY_KHQl>a&bdeBE>_bZ zj*CK8siCcMdxk&*O?u&hK5f~FL%|JSK7IHU2VnFLxUgdy7JT5$ba;1hy8QNg?mWci zQ#-Kmv*`&Xw|sTScKozM;Y9DeuJn`}?S*V8ODD_I0d(B=LXNPWJ+70zI(HvF!p}w{ zyz`x7l;(7>npgF`;*RMZtdjX7-&;KN*j$Hg!r93(9Xn2?lOD9vnP=0ko#eLA;i!qu zeJ2-AEI!eh?{CTPJnBwIIC*-ZbL`~lH2VTO26bMx?NRM=+(G`C?45ld=jR~aGg+D5 zJ_F6$r#)|%?{(eNXLd}_9oe~Sc4m6-?6J8$hxhE=vukee?2)-+Gc$V*@7%F>dV2TH z!@K5YJdb(5;T-kG?Jc>8!Gm%<?^fR%)qxf4t!85+yQn|=*ppA5TKIgo`zWU&JauII z?8(Qwvl;g|lWn1!!)E55)=JI2PjHwXSB78V8(g^Lw%r$>WGlK4zp3VsKmekDxY+>> z+ZIf>X|^`UE>ySQw5T9IR63!kWQV{A4Q)tQVrY8Bg0hF-((Fm{<j)7}Y4<+qdw+af z$L${qMmb36<nJF~5A{54N<?-46PzBxCi9~LyL;jTfeH8Zuub~jyS%+ZQa@ORG~xqf z9-*F_Gr#-HwKDq~%g~z!u2%*+XWh03rMHP$n<za;`t`7oSryulnb<e&W#a>c5gYRp z*=E?7pYu1w(iq=QE0JU0#s>5Oi??2j%1tckq}Z+_0V42vbrx+Opb=$abojREghZy~ zjsz&BM{i{sq@+xl`oRFkrqb?ohV~jJx%av4liz*T^AG%vvi~LDFTBO~A9~3!6cyt8 zDZX!#_GT_kP<35$pZo5=<Nk;LZsnFQR{!+icb|Fw2c7c2{*7>SA3OQ@To<!>v@2uU zHw@dPm?s`Pnl}!dyzx~IQ0Sg|>IlcLbZ0sIW^SR&sh>K}Y+>8%f({Pqnt9C*?&@;b zSU0uH-P~EVSj0T$K_j;P)X^i}{a?C$$L`O0DbZucQ=-L~8^8Xw=bqp9&7b}1#NU4N zmS6R2`DKMW{pWmWe-4hN9s4+t{ltj_oI2w@&gqe-=jQB)u{6iKp8(B97Ur>i82&%! zfNu>n<$3WSDbDcG+ir>%+?V)%1?_oB9ZmXqsfeTCJ;+e}5w3T#Smu5fFCJt``Xk)$ zVN`n1AXkh3@oSkf*<XFK`>pQL`Y!2f{&NQpK3KIJL_2A;fNDRZ=VOd?^|{${9U@h` zS{G~~i<XUOgzY{n#b0YJLOM~Pg(w^lxCzrg8O$_GW!>K{mczgUjLg@_>!uAfSWmZX zv*^(6tG@O^w@z*unOh2z-&whNzrL21WE+(4gLc(pk+R&vDV>SX-<seS#0)O!(efMN z5LdL^s=p*j%fQ?egYwPuE+MxhO#JCGHKE0{@0FhIaoxxJn()QEFY+#wj2Bv<^8_?z zTN1?UeUzYGpvPIZB|45n$=$#mp$vU~m$#_+J>F}ZK>oakew$sJr}?FMdizd(x2ngY z#%d1tgI1?(alXo)rUqJMx!po)S<%xbkEBU+KBu{hNBtW8@9EeMN41#Bqa4Qbwq#37 z1Ua4&dQZVGP4+yB>^up~4rtk-rRaQ)8Z8IYbV;dlWpb(~NuP%lUw?DUAaD9K?Lp&w zHcV(!rS|4(DQ&;D?}gjrjak#1@@?dBp1+^5nrR!;PHHy`-=}%2(k#+B$3o}BeE0I* z#gk&ST=Ez&GnS@h{Ck0)X1VcBU<HK}&*Cq&@9jv8<W)O--R8LWqeE@|;U~v|UpSk4 Of_2;<e*XV);C}#@e7AuB literal 0 HcmV?d00001 diff --git a/packages/spacetimedb.bsatn.runtime/0.11.0/analyzers/dotnet/cs/SpacetimeDB.BSATN.Codegen.dll.meta b/packages/spacetimedb.bsatn.runtime/0.12.0/analyzers/dotnet/cs/SpacetimeDB.BSATN.Codegen.dll.meta similarity index 100% rename from packages/spacetimedb.bsatn.runtime/0.11.0/analyzers/dotnet/cs/SpacetimeDB.BSATN.Codegen.dll.meta rename to packages/spacetimedb.bsatn.runtime/0.12.0/analyzers/dotnet/cs/SpacetimeDB.BSATN.Codegen.dll.meta diff --git a/packages/spacetimedb.bsatn.runtime/0.11.0/lib.meta b/packages/spacetimedb.bsatn.runtime/0.12.0/lib.meta similarity index 100% rename from packages/spacetimedb.bsatn.runtime/0.11.0/lib.meta rename to packages/spacetimedb.bsatn.runtime/0.12.0/lib.meta diff --git a/packages/spacetimedb.bsatn.runtime/0.11.0/lib/netstandard2.1.meta b/packages/spacetimedb.bsatn.runtime/0.12.0/lib/netstandard2.1.meta similarity index 100% rename from packages/spacetimedb.bsatn.runtime/0.11.0/lib/netstandard2.1.meta rename to packages/spacetimedb.bsatn.runtime/0.12.0/lib/netstandard2.1.meta diff --git a/packages/spacetimedb.bsatn.runtime/0.12.0/lib/netstandard2.1/SpacetimeDB.BSATN.Runtime.dll b/packages/spacetimedb.bsatn.runtime/0.12.0/lib/netstandard2.1/SpacetimeDB.BSATN.Runtime.dll new file mode 100644 index 0000000000000000000000000000000000000000..2f56513350acc70730d652af65dce9fa658f1928 GIT binary patch literal 68096 zcmeFa33!y%`96Hkd1ofcWSz;%qJ#uw2oTu?WElv^s)B%mq7DXB5E)2Bg%}tNE(k6t zE^(;=f(5lMb*XKsMgezQ+^Du_gP@kWR;_KVrQiMB=grK32|xS$_+QueUDrPsbI)@> z&vTaNIp;m^GVl9x@|ia&M=9mO=Rf~Z>M4B7Uzm7gh{2rI=~r3m@wC@EKcy$V)_K~6 z^Qwy%RYfnTntgF`<?MwEqn8w)H@CQI$-?4!3yVjeI<@%X=$yIbnVJ4BZq<{=C^bnt zs@;(OH@39atV)X0b#G9uRI1fp*9mel<YIg)l|$Lsev3l-&%X%l(955rB3Fs4{7Zbh zR8;sIfO=0A0rl>PS0W1cFRWDt#MEQ8n%1KGG2g}UuG2ded|Xnw@=NAkehK(Z$T?$C ziGH`Z6)P1fud1%9go1rTAC(Wp;5+FrjNvM;n!6whN9n6VpP7TyANY+*Dn@ah<Q0F? zH>14j%SB2R{fwEX{=bD|P3Tyq{AK6ZZ&P^vU{uz%z`45?4SC9jxO(SsDr1>s#hmn% zwN#cUgb@;TP0P1ec!_apDJ1-mCHnFE^mv;oP#aGz<`k_$r8e?J;WSs>^6eKcjZjEL zHpE7@u{Kj266Hxzt@~{g(h^Hs--+T`Zu2eQ*)7deXr*~f@8sr5(LBGG#bXb^3&UYM z?b%oxlv0V*(Co2V6c#F}p;t?v=f^9{mC6dPFG2=t^A4?uCXdyk0-;h7y;>@2`7Ylz zRjCf4PBLSZ%I$4b7!wW?B~V=qo!?glc~P`8<l}r*g>l*Vq9xGvO!IXrj+WBesn!*u zUBJuy1!d*N33Y{Ba8Q?>Vx00!(=*jL(c{EiP*zxWQm7k56Ab~Q-9<alI7_5M*5^`` zDRg=Ut%pOu5V~ngBZM9zw|o!B{6J4+dZB9!x#e5bD98AE;@coTXGuE<J+qz7^6fXi z&@gP<Wlk?sSXPC6EULn)w5S^H{$<yh#p2|IhQp<&$uDc+BghL84eK6_y5-M=!Z0|* ze#e^{Y2P89&?%Yq%QiLAM~8UO%dFVKWil53bA1DjRG-88Y&S3%>_EZ%gC5lFQ3Kgi zr3#x5c9h`WJ(%6qhP6^?VRbJ=9>VDN2ptcpV0mxBgSxs8zLCt_-dg3O4hOk3p^=E! zA5;emiZOfa1b|Iqz(yqdhJcyx_jMXk>8l<L*Ou9F5@eYj$7L%gG+qoFLw!-dO-r;N z#NzJLr*A&f)k-3%ol$m<JAj$Sj-V}GQ1*OM1vanI{-~yBVNw-#ET~6{Wa3~>%EZBJ z<-}?E7ByovO6CpLILw>&NRP}L`IdQeiuBvgo3nl}Z^X$7VJf-v#`4MJ@p&@^b<3X# zLBhNd>^IcZNb?RcbS_lAx*ejLE0qLIjda!_(#xGhQU|)uG58VnM}AcO;^j^%)_+pA zx<Wb`ngs)=7TP;>Zb`s)Zi%!~ps;!XMkIu-@JG$9(@^(ebIWfRm(!)ymgzH7`nEBI zd5!5KIf<T-m_Al3U8R=ON4nBerjH0A7MJOhsK6#QI>=6+L=|>OsCTCi7B87TSjaei zTE0cyG6ph#e4IZWq+QG(`Ih-}j&$74pEW<2KjP$YT<yAJxr{?kEK~($Pn#NP#UaY- zl3=pBNKYK1ySg0WFRMqJ{&w~F!FWms?0AZ_QlL<_X@3YS!H*ix+30-h<0&&bKAsm! zT3VI^iQ|XMf%HAG97tC<o+5-;Jh2=k5^?BqkO)6{Ily4acw+y;@of1Pb@F)P@CEBZ zM`;)1Dc>@l7fZ+Oc>eka<0(!~Xdztecv>DMm+}0{)JQ81F}~F&sam#r>623<nAqwM zE!?t6AGvURlg9i!eA|`I*|8RBr9ffz5E*N%=s#+#7a_3L#~R1piLt&6B);uROh4Rq zC5pthD_!AOix6V*#I~DA&7s?FA`vayZuu@8D;s<GbDlk(FebVbz2Q-FE%t#tutSZ0 z>?KGqUC{?xQzOLb<>dA7Ig~@MN)RU{Swp`S%|h!MIuUhLeIgU$3qW)zXj$JC(0WX$ zT*Gk!iQ~E-FRGghug4*h!O^?xh@Gyl`gK&>DEjJpadH>KRnW@DlXRqYg|sh6T5?<^ zkw&RG#f{YV+eTHs3&%WeCbBx^$WhfYDUGA<P>l%BndM>hwQH}!<Cf#F$8*p(M*4z- zCV$tMSVCURiJm5}%v7(D0-R4q;RsbuAp_MY<gdOC7Qc`AilUI;Ck-ucY3NE3<fw5S zgu`OFRcsOq3e<`$F-f1Jw4k3+9EA9B7T}cQC^N)J#k=};@s>EQ5utonaZ;egKMWKl z?h?mVJJtZpVI6A}=fgWDZA57~LdWitlJSmBFxfgZ3L$jz&!1IWj*OO_zM{f%QzQJ$ z&}fNpf?0t`Jx<7r0kDn}tj4;9d@?Io-I$i@a0urq8-<!pjfiK4PI7BPgDo}DXTl0< ztedT=d>0P8+^K=&Op)609g%N)e1VZX<ecHJGQm5eg~QJo#zJ?5a|X$UxNr_U@xxUA zGAY&L#82jcbS_HE4^ll|d<vQ32_j-hC(1YGNbMvD?iob+uCwDOK{!zoi<F^DSzi;v zZo7vZ<;h_$kEafgH$}G*@2Mzsgm`ZY;Xu&uRCS1#cX$ZsUyEac4pAKV_>U`OGH_CK z_-WKMs5tL7x~Eaoq2Othd&qn`xQ!-y2E-Z6mP8t5(cRckj(2*NnWz+Wh&;Q(p>m?A z!*oPvz=e$^)(%ONP0>KN*DIyhWy>5-KOCAkJ<~lsQ=J>;d2X0jZVJjAPv|unzTDo9 z!U2~`!<taw?~!GE>m1)Y@$ePS@cYaS^ZI^T=+3KR`1#@2Dd2a~3o?3Sr=+gS^mXcV zX4(vYYT=BG)D(Ds;w$s<7#=4MveU*%gQx5UvjQ_5Gkd1=l9m;iSx`J_u+dl_>tbre z96A$@*2UC_IrIxSvR<4%NWBB_CG}$BwNyjP3e2(=h4ry6rbf)H#=4jqG242}cj3Up z-KO3sau@lkZkPuqvAlOJtm*^JavXVj658tP>;@P*3q7{yorllI6XWyguygiVC~}1P zDBGX7aLT>+!|iWx()NdmWVb&FBudK<wm-}V+5WIt?30pG{kftV6-vP-#l2Tdf@|5X z3NYU381E2v7(H;<U=9|{mihW2O3PZ)0dogCOu5DusD=gCC#uiGS9-Qeaa`X@a9?x| zFysBal%BqnC~EcZ@%8litdv@&bVu}AWvZgCGQC9hsqV*HxdYBwafs7u8*URHNq7E- zCgN=rc1g4mDz~y6XH`hU_}e8L4Ua$!xB3tc@cg~Xc=e&`44DvAC)}dv!y$A5p!!0< zGE;VC!stZjiFH0;Gb_Fb63&%9ArWi?3m`==7X6*h@`d>3K0ONIpby7u{_4e|i-{$g zhJmnitQ-28iM%_}Dj0g@FR#WI#t3;whdM+r0WAyagZa{Iij;9wFO<oWG9I>^cTh){ zz|FFri7Gs99hXWSC?nSmWQ~xrhqr@cJ%8vOnNH;)3|^0z^aF*YOiz@_PV}+;^2=d6 ztdG?=y0aZc_Rvx3C$>x5&ukv`$V$tOy0mA9ctz0QXvlx0{vOum5Be+ivSV!;%Ss#% zTatWf>APGi58y&d{xO)AzUsHRt=mzhGsmrU->+R?9Qk1Xg^FuMXx%p;iyJzzyv27# z+rG!UzOfeHm2LZ;EWR9zJj4><SV%eSU*yYIjb#>x#18U^<;xe9s)S(Gv80XuUKFpl z81)9-9ASYqn1LZ&X7{DH2r1Rr1nhoB=4|evzGw>@hcA?dA}9fh4a3s5#$dG@|CJK9 z<h4$O*7LgG9fMUZzE`#FYutLTZt=aQZQo+o_u3ZU)ouF@kLRk5F&HfQ;TT|{ON;?l z)+3I=ta!aYVhkoo8?tS+J_ZwEP8tIoOB{I&mbvj?*AoAlw&TCU^<CTIdwtu!KNa7F zoeoyt03Tfc#N6mmd!xus?M+gQ`<#<7kC02OW1(~`IZ+uXPqYKj-=m{1S_C0|`OUzL zpm9=?76)In5ZYdQ-uEZ=KV>*p#zt;1GjUGnN7aX}cT!K%nkDN;^%j{{?s_M7oMzsF zFDJAf7|M`EdZ5*KLbr*%uWP?u2+oxpCo0n{dWZ0n92}D|2u$XB(XbC%Vmxg9ku}A( z?khVpbSHf6Nh;4+qjyQ!b6kxWJ<+>?A)dRsb^dTodtQP$X~jM8bOXSCMFOBE9)PvG z0Z5KK3bRJ8Ib$Cr!C*DH7hiH<*e_*y1H{argWXZIWp}iE19Z8bpa)9q*g8Zvf=4$2 zc<|`!`x5q3a!hdv>cvq4##heS{O<T}hUo7xCa6O9K|<X@PtY6sDdd1BJ>bn?lexa2 zPllY0%KpnXni5QLQr%X!K#NMY;;Yxs-_Me(<k-V@uty_hEpy?txx7@4I33jw{lUZu zumw98Sij5#)}J2KEiv5>UG-boeyV>Vc|ARZoo0`iWFv&s_lQXjTE<r<+adcGOpWry zu~=DOCxrcg&z`@Rc(`W!psm`3<B$;6YPeUAMl-Pya@2;?sUA{$fg`8HErl^DaHyA5 zcze9?u}%oh_rT?T90{W1_WaRNUC}RWM1kt(u{rvEPAx{=<EtSa^t}Kjk4d7~pY%YN zQ4RJ0ZZ!|tYM3{vr=8UEqFWE9JL(}G^t}|X2m1u6M@|EA7>4UF+Zfue3D_*{#UPvp ziiM|v6u6CsQNNb@ixD<{8~d=W<Lt4pAK^G#j#Y-EuJ))4nRpDX_snprzKB=nRQ)X> zo6uCG9Mj-nL6??aSM1rfJvW>I>tWX<X312LYZCbN@k!L~o)JEPZrQ-14??tOgbbPm z;>YhBacU?B>%Wwa6^0VU9A+i@GZc0&c~qYxO`QhkU&Gm02WAzEj_vtM={1u*Iyu?c zm=nns3zJPj@^g5uQ%oD@*grDY9oo)yyLi>;@p&z|#$>X;WF_bNf*<DkqSkYbd5K(0 zIMIjWxn|5P&{nP!#T;Ask)&K-d|0lf_F864axFbyDZR#i*Qf~QS4YIWVho&|(07>p znQ2R~jF?<cz|%85;0dbeU!d(#n6?C~qsjFKygj=Iyg?oPr>L{BW?|{_@q$lAkNltx zVbb_`-6x}muliARY*{Mz@#mtgW6+l4_bid=6mLOC&qb$t6nd6O<ZPiES}@UtqRvXG z#%k)T-XneMk(XMHiRP>Rgv_m<z4xVs{spBRpLrGP=NcA2X_k#JalkAGpow#3oI%UH zY0GR+rMaMB*XJ%MZ=w@rvcIwG(y@rEdN*p7bqPC5tS7{SzTZnzSjqiS?33Je340N@ zn%%aVcs-q@o_E}Ou&+ct#Dl(f<Mm+8Z&{Zxd{~#>l}gHYO-AzK>ylWwsZeNjUFsK~ z(Er`}f6Karvp!ju{)pBOUzbjdSB)8mTS)A>baHFk%9XGlwk}-_Rm-~cK5BQ@rJo~- zrbgn4Vl8ynB?ip`KiF3GO2-OAiDHf|{5T4?tV`EQQ?f4YgEQ78slApdJI?L-G_Far zlM_89IniQaqA9etCQXbd<G;)Eb8RGdYP>#laV-K!z67}Lhv~ev^>jYLbV{$ISp3~| zGBg%wE1ijAP7MqHA}O7>AC^vWsAXa#ozmASOeZeKB<AgD$>|ge(@CM#d0QEe|LEki zI$j_8co+JZoX-aE!=`7W1nKTJlcuLljC4CGpNx$Kj+{>^=G3roaz5{qrjn+oIMfQ1 zNj{~oHlLMH#n*wU$@vru^GTuAe6ESde{}MBd%QmM@qY9%IiC-MAC}L@B*-7;^C`|x z>9$<AcjqT#V}Z8vnJDJeu<);v=I0aAR8l_0p_ZAEd`e$!KIbIob6Rpf#ln12Xf>Y? z#N+?(^4YSk7Pq~wJ{GSU-TfuH%j3NeI>@_el8-^2hV`&SKPS=uFwr<9!@4T*L~(@c zCYnLBKwF7U6mx9h-y|h^hcuOxXsNxHDU)Lm>G?|OH5Y@q$;qCcoNTc$*%Vq^Ters( z^1sV<PTRSDJzh0>{33eHT;l-dhq-<Y*28lBx+LI-xqh0tmT;oa#B<G<S)i?4CyF^W zEZmfo>o=vTq+E+bEi)#$mY%PaUNhI{C+GU~<Xnq|xu(!su0M_^<f!NRukotU<KLsl zZRdIqtcT^gSrX7zt|gqLTr*}CIC8F~m{Y^T$+`YOno7#GIMfR8JVtt+$n^zK#~0-@ zl5;H<=9)rlxptg3&SPd_;D5w9;}Pa{TD)rH`XltX?OcBX>tVUZ*=O6imT;1C&6ru> z$hnqcP7Mnu=lZYGR8p?Rp;myomYyeaeIeBGT+c|(wOE*I3a#b3FrJX3KCh37SB+fb zWc>)a{tni|a(z$|(AK<`aFTM(m|5V+xt3y14GSmdT7KM0&b2ty3NY8w^F*$3;XIM+ znaQ~p3v*4OwOsdzC*-K-x?j9%<k};rP;xD#)%7|J*28k0DG6vR*Ah-rt{F26968rg z%&B4F<XmSTmTPgS6=1HV=ZRd;hdMs5&rHs>SeR=Ht>wBRo{*!S>xuEIk?UOaxb0jQ z!FpJ(J4yoD%C&@(lxxP!0!PlZ6mx1=I62qHN>fQ|h&a>=FxS%aM6NG_I-cvZl5;H< z=9)rlxjrkNkfWaK%6QetbqRXhcCNd@dRVS|NCMi*wS<$DYsSn1N6xhrb81*PIoG|U zsia(sL#+UFEj>@<dI8k&Tw|Y>xNspB=9)rlxvq{U<f!MmCSEmi-3L8xJJ$nYJuKHl zBmr&ZTEa=nHDhLhBj;L*IW;Vtoa>>|R8p?Rp;myomYyeaeKFMWT%VnsYq2ob6k5ym zns`EvdaiGYSB+eU(c`vrJqFgpay?!W&{nP`oTOYcW)?VduBDh$!@|kAo+wQv<ystS z1(<8;c_P;fp^oSJoa9`Kg}J8CTCO+66LQpZ{Xo2G<oZ<fxb0j|gY~dn&yWPPm1_wn zDc6jd1&*9+DdyC$aB{BCl%|q$Ee^E;%(e79k!xI~PUIRFmlJD<SeR=Ht>yaH@q`@p zT<?rmja;9D9=DxqT**2-*B3|v+RC+rlay=5%mPQwwG?w|SU5S?^Q5VyT#G}k0CO!p zPvm+L)bV*8NzS!cm}?5H<@)#WgdFu;?~PZDTwjbHx1H-MSPz@mmr4TK%C&@(lxxP! z0!PlZ6mx1=I62psOH)a?7Kd5^=3085$TfZ}NaPw9PZRT6EX*~9)^h!IJRwIt*Z+=J zja*-W9=DzAm9QR`>#HOIZRJ|RNy;^2W`QH;T8cR}ES#L{Yow{9T#G}k0CO!pPvp7^ z>iE3Ih1Eo^#ll=uXf4;-p8sR#=<VWFBiC!t<F<2s6Rd~jdc7o|tz1huNx5dsEO6vp zOEIU0g_CoAn>3Y_YjLO*V6LU-iCkAh9nUo`x+Zch7Ur5lYq{<bPsmZ9*ZtyEBiDDL z$8G2OURV#y^(IL`Te+5Sl5)+MS>VXImSRo~3n%A#vow{IYjLO*V6LU-iCkX-bv)O& z?wZK8SeR=Ht>wBRo{*!S>xuEIk?XDKaof3m2-d@L{c}k`Te+5Sl5)+MS>VXImSRo~ z3n%CLacL?k*Wyqsz+6kuZLTGMINDa7aUFTZ5~(ZnEUwU{r;N#0d=gZ3z;_>f#^Qqm zQH23htMR!PpC|Bn1)sh6{2iZ6tyC#KgYlV!4}KO`@@Vx{_}qcd1NaQW#dEddQZ|)` z>oV0f0RNzeM0**v&!GeS&DuXm3az+2p{=JcesWbSmQwp9BH^7|{y_^wyNue)q~2f9 z_jJ+LP&-Vt&r=%~?G*{_+tikcwl<;NPc1sBRxD3wzozye!d<Z<p_SVl{e%82+E_vx zp!N;Xu1si)sof^pD-&9|0o6a~KGD`Cw8N>rNwljH+LJ}wqjtqr3DqpB=C8Oqp}L5w zGge%aP%Wcs^onZ}s?}8WS%E>t=rc)oP<8Bz>k_J;Qk7nH7Pf%u?HHkDa{B;&KXTNm z9;He!e>_VjV?)i&59IgE3gqLyi`B@~pmfYG&yp!(DF_t6Qh-}8Fs{Bqep*fwOJSf8 zmO|Vz@l&y+(K1ymMS&t%ih@q`NwK8TGEFS)0_|XF7xYATiY0{>`6(i|eV{!o?StOv zTM3I?IM3}6=m1NHpf9>FVUf$cxg7%?Vd)r5iGG!^$VJKAV*<y(a!fEas*wY?9l3;- zdu-rXSdI;*MRO7sx!{xADbNX)PC<XPlUQu8<?=*saiAEM;$V8TTr4(HIhoGw9Ow*7 z=U_&3m{@Ff<kT;>Bv1lNNiZ`yNh~&_a-x!38YqROG?*1VODq}8fgD!nb_sNWrAsh7 zI$tcAv_!<xHP987uECt>Qn6&wGFvRi1&)K|xL_c9typlK4<mP;Sh@we!O|@ljN->p z-ynnwi|h_^LxB)1p<r(GKCxiJ!y=1UZuda<!6$$Qx(D;3Pqb8M7ny?44oo4t3iv|# z$atAG6j}opf|SrsIJt%*Yv5Xtnrvui4O|Y=>|-aks`l2v6(QgWU7l#9gSBu~2zW!o z6PAwF!j&Q5OST+iEnFP}Dan>&t%WN@AT`<2Db1R=Oa#&nG5M`2hIBar{~@MyYvO_t zNI%4sVNF-ERK_8uOl#uO5y(8mlx0m^KLS~Yn6j;j>qsE`5L1pdaXks-9AXMs6W5hM z;1E;Lnz+6Mf`^!Lt%>VQAombco;7j33FKK5*OsnWV@ffHs{eoqB=;uWh0uL9#Dl&+ zLTT?!Dzf(!l~+HHDHFp<VB(D#-zA<K3B3Tr@)rR~4}-h}TK%%<VqyukLsg;n0DIq5 zUnlWoQrY<}&y?1_3Rfe~diiUA3&4Gr>@UVsbwPhkjX3ar=)&@e1!X4RmM}HHgPu3` z*6xC23Tj`+SC0{fcUI$$SL>1VxJfN`AAVEF@`dq=YNS|(!7aY5z>u37sk8561-NOo zMv9fSuYMC>%lcZ%=Tms!Tm|}x+`9ejbg*BMsgXDsQNMI_ivjw3q!jR`aXhkCb0h4v zZvh}<hvcu7P~;)W(AyB=_sKrl@-%PNPV6iA$fn^<uAvlcXo<Vl>|?zxch@FLeJ!O% zVVDhXX>GMs`L6j`Vp?t%3+<NnTfLVd)7!>5&hJs=2){$(perOUd`9edm}Z9^%Do8{ z59QGRcf?Pg(Ud+#X^9`RwLGW$uK4g7vA3n9RR0H2wS=GB+ow*!n8;uJp1&UGJfGe( zyctjKRP!hDhr9kx#7%*Gn$LB3O11|~jO6F0TUwBPbt`QQkT%9K+jjy(hgN|$hKa-Y zwj2%}MBB!1vFOWRr~H;D{`%qPaW3|TTW+Y}kcogsCW?bB`mV#CLnAG!!j+~)JP>T% zp<UG)agd#W>u^{c?W-OT2iY084oeU1Lx-yE;voA3*I~q=4jrpL76;iiwC-?BmHg<< z<Hyz=j;)ekYI!Kxx<jY5D*1hphm~!3_^aesu@$j4d(jDpXyPGzk2dPasFGhGc<9-N zM`o2=H|Jq!8y;Cza<!L-q-}U)SIIR>9-+43ky9mC)_B0$hDV@Et_Sh>wGEG8m0ZQ( z;cOcoxm9xR&7<2kJo2jKjFAVst$Nt~fZRrpa~_<Pz9L(2`7Z2a;ww;%Sa?=SLAHY* z-rL>gef0msS|oK1$M$rbtk+KHH8g+t{i^tZ+kF%r=cVt%f#;>2Wj(^)$e(;))jn7c zTg))A<L9NjAiMXgzK4>AkwxE!Xz!zE)GQER<2JhY(aXw*`&ARg99#HLDD1A4)d!?0 zIWLuE5cjJ}x|mj;mrBo9O0RjZPeXFH7bRy~EX+2A)^d%rf!3eR|B<=w)^@HBxsU!c z^q9FWX)V`Z!FpJ(zlEyh8bDI6o0)3~C%QMDYsSn1ZRI*q%&B4F50Y~I4{0hX*Wyqs zz+6kuS4yvWJb*ob3L)kd8(`<%#JC24$BGNf^Lre(Vk4E~WkKA7V*Vb-uGmD?$)fs% zszXl=<-G7r)Y%<%dU!v6Mru!gYRk>|c^=-3pPAP4#I%+>@!Q$93qt!5gKg3mnt%q% ztf9~v*l<ed6fxNIPdP?}7^prd9l<eTBl_m6As+Po2TFU4*ufqn#&?LV?hj=nW#fhw zgeFRa@m;EWr#+*09Zo&O!LAibjhZ@PlvWx&v+qj0w10W;@_xPh^%*Q0ufk8A>OowO zDaC0+(Gt9O5eKlPQ!lBSx9|eFz@qkCk1Hr%=%!Cqp<DPYfGQn3ef((H27x!@D7AFd zg6MhTj6Z{0)6ckfV|v=LAm8bJ@}QTrJ{g}Xe2@%<OsFmR%!IrjAB>H9#KYrN$)7qN zpEK}5w^Rx~EAWvz<RiBB_#lV!Tq^#&0UMo9&CHsU=To)Wf-j|K<z=f?c{m19k&N~^ zJ~b!%R{@`zktz6amf&TS-^~#DTjFihr&123i+)ME)V3~rJmjC{3qG1IcvJeP(C_q% zJe1b2{hy$fFVY3CFA%-ag0H7Z4Y$+t=fUwsJ~f#=yf96yA1T2Xg9mecsy_Hz<5StW zf_Jj)Tdb!cAU?-1x`(KbP%}uUdVQ+Si1lpZ(14WvBkTM|iM3OPv_6TRYuMt*+_CvS zbxE4weHmG>o=NN1z(1;=<x0lhrsfgG70!|RH?r(t>W2kI(}z8|F=q^F=uWvbSM*EL zE)F6__V6ic*5pec))a|#6nl71P;&bjefm%z$dSyX1;z7PTN_&$Q7EN;4^J6uTwli@ z=QAGU8<n1sKQhkV!$=8}8JMFok>in&F|EM7$lXZ1I}Z~XY*1GFoRP5j$U0_<%_na{ zfbBuD6nXX@Y#QrKlP6%nK42}G^4uoa3-rvwlY=p^?+WJR&Bh~z*nGhL;@T`*?=oyE zunl(^W--{ytTRjIGuYCMPtlhwd6Ee1MzS1LO!hQcK+ZzNGfO;k<#}JQ<FYNo$_X}+ zo}KZ0z6KkXagP~^>#DufO2pAo-(zNjm8&$^=9_!W#bATUrjm8RGe2UBl6AxCDQpH= znVLtIRw(6qsmodCfuPhWOrwQ6^Y1a2qyB5j9xJl!4wm~QSIU*EpOXEK?0EG6Yxxt( zO;Ekn!(_AB&)(`c^!#^lN6y8#`unQdfEJbpaXnh~Rl8^_E&Ms^?5kcU>rd8Sy~Wn6 z{69BW;@ykCXSv*TDR+YUkStqU_7|2b0lOHs&&XbK*<Z<iO*TmVjjUThJO`_PlEsW= z-;)h-*?-8Eu-ss*1CqfU*XAYrm&cY%C7a>e(#dLFmPIzuWdX9qZk>5#8Lq96>?^m< z_GD9C+c9JpyXA_>Qe9iAPDRf)xVGbH`-E(WDkED_B>g;5m6L^Ygbh`F$@bH8s2V`l zoplbwHFrPid=Z(xSdCD_4q;*Kbk0#8Y=*O8t4OkqR3{z6#wW2+>SS$D=VZT9Yw;$J zDP-?rqg;!3c1Dss$Eau$J4sb1v2p4$?ZvKQE>@BanBz69<zTk#lOisg1zRKfb{%bZ zVc*rL&QtYdYp^jls!FwmY#w$#jcShi1z8w<X;gF7^JLvv%lTMErPsB{M5DSu?I(+o zU8ue$8%Q=!86%!U$mZkVTG(RrwNYK9ipiFcEl|gk$@SGn<aan(4%tF=G8rzj!WLDt z$f9J6)J0@d$rh_+WHZRB)M_$3?TB*K>JBowQrw6=@J~%*G?%C+jnlbh{MM*n83+B` zg%x?Wx<Nfn)|2dZ^?VY$1GB-wcDo1rmUC3SdYLxa!_Nl8i;+-^Wp}D~$VSq3m->TA zjM&|34|^uFV2i%jWiku4=mzykQa?AS?^uf*+ssB32hDNncX=Jsk5d^roq$^d6Vp1Y zw^K^ewYodCB)zlhMm#CCE9B3i?~G^G#TrZf$T>Z>F~21J^w{~p&g$jV?!aHL?1wD% zC^ePv?5wP;(yV7MlnNF|*$apcaX9fGtn<{MSpPzKX_3e?XdO?yFh?|{fs*v8v43Vu zpNHi3LF?ZJ`y^@lQ`0|5)3#3+^%C;nQ)6@b00-rX=cbfjdj_fzX@j6S$zKAj@(%;9 zD-!%5dszBFl~y<`y^s3J?DDpt#}PS}c7;#Mj)GO>J}S>SA*GM{zzk0rsBSW2(+jYg zPfG8H7N-LT<xNM6ula8FbXK`3XQI>&b1w8J`R4$u{88Y#BEb)`qv<|hdrv21;9&M8 zz*zyoy9xx?6ba4?3Wjq8yU|)}M2-;$5{D2MYtb(uW>AwuoKK7rrxIrnFQ#WI<j!ip zPclCyWhvyv#5F0yAfJ`80{ES86|g)-a1-UHh#iQ1h(Gg+bvyA3qL=z5#I?k+#B+$Z z)8}1aKV@>R1s=@488|C&JMgXo!8Od%ykH~baL!g>w}4=+5sVQB5{D2MYtb(uW>Awu zoKK7rrxIrnFQ#WIWEsZ|-U9WI%1kT33pCbalT1yYh&Q^lFZ#7dt65%=9|&IK!|^2X zgS;yszX#On1}#=)CZ^%IF8B~;R2%4y^<@8J7^!!29z(`n^F0O3P5BLaZgQRjPWHbF zd^hKHx1Pzx*u>S<MbF{rS*sawb`AQtD9-B8%N21Zwj1J1jwSDjGpS{BoaKN$ZdvNI zwAqeP+g#Q&AZ&-r`Ui4wr1@G>IY;erZH4~Xjz{f}+d5`=)OW6JgCFl=Q>L@R-_@ym z(SlF;Eki^(I3{i9+WrMwit6mL`*PkeDXP26p2*qbO;LT5%B89aarOY-{5I3|d?CLy z!>?w?ZD>u+JA^Ghge^UUtvrOSj<X!pa))K9rQX?2x@vG)NrA8}F6&*8gJbMRlgeeN zm*VUJ9C5$ndY*}XW~t`5Ed=(TI4jS{QTZh{XU}B6fj1Hz<1#51R9!9OoCvC3ah8Mg z#c^>qAR||u<CeRiXttBD&Ue`h`9m}E)xx-^52vt~9>SKB?MS<{V4YK-){;H1`sd6B zd&k=JrTNpr{z!IP>gJ-kPNDkPW%Xd~)W6(v<I*;JI;cz>)+_bA3g@o|>tLA<=Pz+^ zF2KL%F>gm^9HR!)whHlW^&G24#u?7_)Oa%49wuaTQZvcwQhVoi@pV?SUG}G-umxlv z>1n~q8J$&QoK4RtQ7@9+mU>m-tc+4s)J2-VE%j2cuIfU|)aTii8Qs(+WGhws!g(1X z^%R-SL`dy%%Y9xDh3#9{Q}WVX{nNE=E;=itOnJN77V5!zs%*07mE@&d4RV>}rCd!W z`$+PValE>cOrovH=&f!hlW6NQ`l>r!_NSn*Mz@?q+gI(0vuiW@@p7ibcU9nqjQ%Qu zi<>su8!`r}-&lrdZ_5~@c9Gd=2dg}sddY~%xDQqX$W|(eZ?GCfCgXlX#t=2yW%Xb~ z)hTW{iEp^N%w-baaCIY@#CLbb2=y$P9nG+Moy?BrDD|$(WHd*q{cbslZ<OkY(?F$U zY&T|9s1alm-<FKgO5W;k<J*#Pl4>Bc5sg#($!tX9lvggxVDBvvjZ>*)64927@hZ<{ z^<Wd#v1E3{PF7=GCJ~*i&LxwG9?3XGts}D|cB<MyW=Cv_+UhbHu_@|tx12;YMSU7) zPiCB^vgINXM{HZhR5i(Emj<?FOjm2kY(!_MU1T<*Gt^&Qn?!Vm`o=A{xoBI)3|x`3 zb=HHOiQhqm*@$MTelC-UW~qr}648!~vz5HG8duG@7Mvp@{&r+;HJd%>sMWN|mg>WC z@5VT*gY8kW=TXbc8Rx3sS;jdZQF~n`Gb^J0O}1W5FWQw6!Ec4q!YWmg{!Yehbqv{h z74`oq<2*HrYzNn#Icf}<jM#yUxoV2b>cK8hbII&#GfyhSpR6x`&6uy`|6o81==HZ? z*O2W<m9_37b&Ja+zKhiDWHOpQoTuDNwi2ukwk>40sWSrKXDm>ATsAr2Wa1|DUTlwB z&O((+wuAF*p$d>muTwLl>KK>RgDqBl+;XzzRI9-*lToQwW63_k`uTFkC2F#@A-g%5 zOVqipr^I)ux`0gLE6BW5Eh4k=U8b&ZnZ$RQijmp)E>qXWSsiTa$RxgwnU|}2GCR+f zstsf|qNVD7>xqcEWG+)rx~v}T3iYyEP9j>a-f)>jv|Kfl*@#xC1J;I!$}?l?AFiiF zv{K=MiaTNhGFPfJGU@fu%qvw#%T$lFip)CIm+T|mId44J7?<_TdnaR+nh`HIIrA!Y zQM}w4nOCbTlFD7JZcHk7joL_NGkC3fi0ro1&kN4ZyjDFKXXj_GR!w9!OV=qZ;P|sy zx=!Vj$+$-|*Qm}es|UMY4Ir~y<Be*V%Op!Ts)=NhrKOoSscF`REUnF4C)MNcc^v)S zl6kYLq)o<sbLK5-L7e&2dUZ*hz3f@9>d5RieXF{W?CRA0p7+hI>UNiX;(0LhR@Gn` z>h!7G)FWgzzT4FIWOi%3O_lVPzDRuUd2UzbE~^KtSEI;me0QmfTqg0|rIuQTh`#XL zEmh!8w$#Tn?@<raCJ{ZI`4hFB>}s{C;HAua)$3%kO6|&QP+tnu>(namJDD3*Q6H)0 zBRwvyIkQoXCR<nZV(u=_W-;LJdGBxhA7yS<C($Nz=^vT*sngwZGJCeDWiFH1vsL{h z?y0iwR}Uq5KB(SE^8A_lmdwtVhm`3n?cJhY4fwMjQYVnf8iH%|Dokec`LJ3`X6MVp z>Q*wzXKvObYJ<z_!G5luaLdV7^|<<t%OszVt5?Wmz8sVFgnG-`kfqA3C)GaJQ>*z| zzfhl$Np>I3dP;pmX0!B5=`#MF_ev%bOfvB+m5M_OjI>(peKPCUs;A5D%X=nko4P5^ zUdnn}-0=6jcd@s~`-~-ECF#Z9rZ|iG_j;dG->}>|^%sAQce|?SFXoT5l-r@^xJ=6J zR9Co6%Dtc(lRRHk?<9G?tnvofTEz2JHH6H@_nI2#+7^3X&w5S8Tvn3)e%5c*j-+zC z)L)Xyy{^8Em-{s9b#*Xa?(3{KRO-M)dvB>ymo4`GE9-65!(}DuCVRK)pXB+DIyLU; z&wf{(5%&ya|3O8PJol)YxM#cU_f%cnvpD;GwKmDKS^YHb8Oq+P9*%n+pZ$S)D#>%d zdMoaELiUI1o4DtQ?2lB=35kgQta`d^v3Fed0X4&ACFzs1|Dx6=d48&X8uy%;{h4|= z?itDcTs@WK`K5X@?s;MMU)3Mup3&^D)cz#TZ`6O{o|k3+O{EO7Ju69Hp8c)LahZ(t zKU5EwE%siW{ZG{|?s<LocWP*o=l5zx-1FA#f2+&mp7&%QRO^#Gm3}1dd0)2Hd*YrC zW#dZb;6#=@x`)dadw-Gb)%{#nlKynIPY+G<Ox0(^J)h4`)91!LU(5FE3zIxEbY0x@ z5nrZmh<m=`%hH}9iPm%QT0CKPmIw4e*S6SuX=*@^b6H9Hm#IO0dQ!POU71uaU(bz~ zdpkQ{%m2_JTVqN3d)Wp0+N5$tdP`EdcKZH!xsS5j>1X5RKFe;e_av3;sQ(`K{987z z-=Emhg2_2n7rIPFvsiZ_vk`UH^1oi#l_4jmvyR2fb;>ExIYX^Y%5~AFlG$=y^@Xl& zvG@3#u6nu4O43irIZnrt%7ygWxMw)0yRMIWj>{?28<RYH>W9f}3%&HO$?UOIFTI_t zPF<&_<n+=nx#bpnPtPgWyIoe2es0e3`cE#CUiZ<T#y#id^wr<QJ*#s1X*Dbn-vHf* z%(gyI_j7HFy;tN6)F-;EB>k$K6ZE*Ga)b5Kq;f-a4VgWb8ltZwvwN-+^=;OcTH>k5 zJW>C|Wj#GN=A5Xvkl8)gQ2mW9$0OXKdiZeZr|h}z$Qh<5xU3#*gg%GN9tn)pwJwuA z*GRpFO!i!BGe_z5mSNAeF{eWR#`TmVozZ$Hne0Wk=8VyQAd};QM{-Wmf3^&^$(dvI z-^px5<FqqEdUkbc#B)aGIGygYg`TH!#_2pV8_{?jA+!79@%nx;iRjs!3HnKw)q_pa zFT3SrUwn!_;4+Em6#aKH8xiha2utg7#PUkcWZj+Yd0c_JC3A}ILnaZuk#m|piA*AT zH)pDzMkag1y*bl#lgmod|B^FZ_Z+G4_mP&_bB4Z>%+84!dX;Ni?ENNZhF<HklJtM) z%+z-zl{-tnlvHk(ekEQmB`{0B884R;I9oR-l{;4lMoH_oy@<{wlewhvn^g&!&HQX# zZf&UpJ)1qV^+1=6@U#!i)?qT6`SbKzTaI(-JiVJtGT%8+sXuU8J=k3Rty@kqe}T@g zkiI;R*|Qp~luR;TnRTJ=X&Ex#BQQ@-cRl4OXudw1Ofug$aFMPelgtkeEYMe22HS|h z#d-~yjcB31-P%%b@{A2E)c3lq-g64r7BU-ARDWa3F`}sMI$HWE5zPoJ(tTW34_2ke zklC%{5<SOd6452Pip)l|M9aH8?OZx9aH(!|Jtd;c^n+v)(Y(Or`dKoGXi;FPe#J6I zv`qhj%tlnBKeD#e#}QGD{@i8HBBC1o4>B9k6?*g-Y29ueSLh{V64BB?tzPM}daxDx zRx%sWO8vOYB%+o2IWim3mHHLSFh+HOI{l67DG{yG-;+s1*9ET9fs>?l8`0IegJrNi zoOO*ZBeM}*s|Q+J>Tf;k0@v!{E_>H=JJ>if8_{aL-j?GSt=2oqB%+4Eb^3Le)q}0o zAG+ma*4>~zW2G;4*4?0kWHzE3bqC83(fxs&^k~;pB3h>>kx4|425#1~$t0qu0=MXk zEQ75nuwE}Cvk~2@ueP?-jh+_*x9S^Q_JHR#usg_XM7Qbvwj3k6O_z_8eo92U1GnoT zE~^Kt*Qb!VXJ`6im&q92rEAD+M0e{|mLa0%z&-kBuBSxw6a564MD%gsUi~teMD%%} zLBC@eY~Kbp=#R;4L>u+j)|NWX^L=2W{+G+9d3122_KdgLmFq;C^kgzS>o(~-$t0q* zV57dzW%Xe9>0i0!WQ?}xcU>kCZP6c-*@(94FD*kvxxxE&y9u^EiRb~{nM@*TAAC>` zB$J3b2Y;qVTgHeU(o@N7L=Wq8tu6HmMD(z}z-3n>qKEY&GCM|(=xw$f$LJC5o7gf& zq2QxB=(75vQ1CH*9GM-XC-fMXNkmWRsbn^yC-p4L5K-UYFZ5Ner$qFWzMf1X8XWwk z-biL6`jvjfGDh@k{S29nXq$e;+ETxBz7K5EZ@SDNqHTH)nT_Z-Iy6aQuw(QaT}38i z6b?SEm%FSUtV!QYW+Qq|Kk71x=sEoinT=?>e$g^SG$FV{|JC)Bh<56Kl1W5Ug3s%$ zlcjYV(F?kxWsK-W-Gj_V^pZZo+EV=(qnGpumjw~gOL`ocjp${)-j?GSy{z9SlZa*n zU(tVY8CLJ$Yx<yDPG;Tj^f9MMU!Ldw=y$pYnT=?d?r#|)IxqOTKHK$_h~Ch1$t0qA z!8i3XG8@rb`dZ5v(cAiVG8@ruy~)~Auf`bd)(^PsMn?yC>&MA#M89|cdu%&Kzt<B^ zm3~S@i-Pay(_K~%_6I$m%#P6>z1n3mMtk(_WHzGrbc1DxXj$-m{gUe`5&cQOP9_np z3^wbJ$ZSM=^*5F=q7Sq)S^8ol+NZP0u1>uJ5$)53F57^J_UU3W8_|Bfz?S0}?blC| zNkrEMKh)2;tRC!Rz1uA(V{}0O&1DkN0qsnY)@?+8(YPot3=!QN{6r5RlcTlyS)b}* zWD-$*@H2fHnT_ak9kGlNeW9adHli<ejkTrrL_}ZeI+qPVL|^K)WHzF|>UV58M)X%* za+>r-B5DYJrORE0{Za56J&MeZ(YJcG%Os+2b(G9T^ml!^Wr*nh;6L;R*Ha?;r`}2? z5j`6GPCrd%Bl?$q)iOr(z5XMajp*O{V{1ztj)?xPzi`=DMD%a{4>B9kK|Ok^#9(LL zL45<6MD$efKl)CW)q`pC5Sfk0F|WByB67?h$!tU(v)?jA^jy$uGN(xkc8q)`pG+cp zDVSo)$ZSNZW{_o!D9wx|vl02tRBKB;4-xszSuVQ>5&6v=G8<94dBm3E7^R!<$Rwg& z!3^V@E`7NzwH_?Xv?sF><(R=PlZbN6STY+?z)ZFb5&a<;G)rAi8KYdYl1w7n8_YAe zk=cmyO`~OusK7i%W+N&z&sbaPC5Widyx_80L{w;Yk=clfOvlru*LIAG%=u&z(Vv6u zOqI*(!8(|$$!tW&n9VMeh>kIjk=cljHQOvhL|+9vnGao0iKy6oMkW#cGuYXfGo*DJ zQHja1j1iTZPGmNsE~baIrM`iPx|n`0+k=R@n4x4gqORszTaIJY)x1F_5vknc%=<2@ z2Md|6+;Xxql$pQ`>C5w6y~|7|G8<736S52u`Ez@kDXynP)XU5ylZXPj<z^w7jp%rD zg=LJWw^>7GBkE&rw>I2k^?jg^xz}Z1BBDNK3z?0muldH7V?=$;@R`z2iKr;IpPAsY zdawcJ95OpbCzx87$rznr){xnV2ATDiA)=Dp!R9xvr$jWw>?D(jy62u~{y=6U8fyM* z86z5I{!V5i!vBGNru6LU)cX<9aFgz`#}UzRlSgJF8et-2cGitB_mfFPy>r9nNte}w zjWRF0<z$RTn*%PBh(?>gli7&y7O=CVby>ZK<ep@@li8JFtm#805sl0pXHFus5sf#~ zEMr6yOeL9(XrhT)Tk6w@Xrj5yWiKM4i6%y7BbsDhwdFWQlT5)Z>5D`(F85?p;<9?M zQ_Vm!J4RE?beBm)Q%oh9jp#J9z%oR1TJBVHtLrIaG|k*YCK1idoo;?kW+OV?G+D-o z&M?0tvk}cOf3&vL_K0YP+2^uSL^Q*EN@gRPX$GAwG1xJhX|5!bh$6XXnj2hJ4>rqe zB(o8nW1e-HM0Ae%9hr^jT=R})h-iLp#OQOR1v^HwO&Xa*v^e)Xb1a#SsM7Sbj1kQ- zCz9ES=9;nAmO2>`%{8aGY$hU_Yi5wyh|V{Swj9UkeDgV(M09!X1?D@K)q~A5S?AiG z$*jA`^m3U*bdfoc%to}pjJ6CBU735ax!CoTF<NMrkV!<VbED=uG8@q%bB9}QvG=Cj z#pa#a(w<yzt<SA8{m-{duJBx9=91YfE=$Y>hp=igdj)cdkv9@6Rfjw1@Aob-zjE0x zR8iWc=7qSYR+pK*WPA1R{62NL+3&J&(HrJ+v*-e;WpCPJh3|u1Zkc|#Ks;Btwl50z z!M4`5ot*oHS!(|1vNgH?G|S9BGTXBn^9fm<x*a{MG3_pt7Od?G)0xcLt}v&P*|DuP zXOT$;@eGWaLuN;_);wZ8IhwWRmt-=Un{t<%oi3{fi<x)bax$7%n!PTQ(Y(@pK_;X5 zv)nrK4{O6{Hs!7|Df6VCGCyC)y~<>h$!}-x=3Z@z$ZQPPm|`*;!!>4ElI>cvD#>=O zc|Xav+Wa}mw%R<mpd~NOx!0N3$!sRpnD?v=S7txXU1L6USx?VrU|*8iysR}tFP2*D zcDvSGO(uEyHurjSv&-tiZZ!9i+3j|n+37OL%R2KWndGG@_hz%lGUP?&-D128r3IUp z^+s-fz&$m{OG@6Yri4spc~;(SrnhCV73AG+Mv&Qv?l2Rr4OeE5$-Bc$b=g8sDcCGB z8&SR4V#{%)>&;hW5>d~*JIz6t)q~w_f>GNu8R?&xJ}#4pequ(D*@*5n<19l&1M?cp zV%JkfdV^U?CJ_zG+h}eelZZy=Z8CSe<raG<<u#fwT~?AlJ#Vv_u}E6CS7v`|R+HK7 zc8gg<X6MTmv!2Y(mn{ZAW2;D-;GeSvyZZ$n1se4w_36}D>%>B_zMU`F-z#YKHwQ(o zqyBfO$0)0@vej1=iI1RR4borHG_mZy0@42&s8zDOAV)NB!pHvM?ow+04vkSK=8Dz! z!D4dRWI5UA$F``||E`t)bM^m!q2`zJT1G_fQWm)*N8wtI$O)V+Kc=5{9Hq9Fk+n7d zn6fe-?5t>+54P<8n;Pb`=gAR08jx0!HP+_?&cOe!pDi)ksK4+?&2~=y*m@E*7?ia& zB+u|<*={v2Gopz=qhddZ?pAJzwhzfM-ayYE+pE^L&E(pwJSVBG!&}5w&y`2!PfJb` zH6&YGGSIp;d8Cp{{a?MtGXjUs<o}x=yJ{Vp2g5zu(VAWO4?pAc`G31L$(C;GsX9uv z*?Nvvj1@<zSGJyej#6#5o}EXjHe1hoN2xYj&%;NlHe1h(qg0!%=ena*o2|!plxnl} z6da}6Y(03<`%&3KZ9SWhQf;=L!AGe!ThGL!RGY0QdX#Fj^{hNfwb^=_k5X;6o^Os) zZML4Lj#6#5o>z}jZML3nN2xYj&)G+*Hd{|yd#ew)8p!djJV9xX4i81GtdGV2?Os^w zPp-$x|9{-Sf8Z&Sry%i!VLP=HpDeulC5X>bd<yW+jQ02}#phVOF{1>ZrT82NR*q-k z`l@0z1n;)k0?QUvinrQz!5e70s!Q?Cj4gP_&=$OR=4!kn<0Cwd@)1h>1@D&mT=iC8 z;XM=IqV#w8f58u`{&;)C0OivIm0zErvh*Mo)Pq%l9-`Xo6V<VLs4CIJ)Ny*aD$^s> zKpj>?^hh;ak5Z#_1>R{e8gH~2gEvr&#hbInsZnaYItlL{I1T@|`sr#C-mP)6T85r2 z$NO<`FC?C#KTX}NrsD4LY3fcjUHt@J8}V%XeQJifU(LibxM!-L<Kq33YL@yH-h%uz zo-2Azor~*B5%n^jb^on8PrZS66zo=W)F1FZg7@*>fxh|z^%}m*@%<9McdGgDimLO~ zB1ntXLOdZV_oM}YlI!Pk{m6_wf7*vQm^hL+o;Za#lQ^3=kGL3^k1>#X)&V=<>3)&7 z5`PKotPW7_qo2!tKrIEnYF6tHaAkF!mMf~WOjgnRW}W^muAr{dSw;KI9CJUenAYq2 zaiw$v{C#*LO>l#HJZ+S-m-1d*wLR6j+sJcWBA?|v%V&%3Hu9w9-A10GY&2Pg*N6|E zV`?<x(r$A$sQRM2;VHEpK+XB;L4)6Soqy_w@}6=YH=h^0?WCD?fsY&w>j7sQYuLuJ z+fb*^vxEMxQvWLTuTuXO^?QhErhm>P&+EER+G(Es&_p}|qXXxA+CfIP%`<s*o@e<x zT4!CFe}kv9J|_oH$Ln7Cw|lx%(_IhD{|TN-k?7X*39<F?UjkX402TD9PmMgq-`NaK zd&cuAYJM3g{@u-@oL!LR*%ism?;*=GC!eC9Ux*K~@jd(hJ^TMX`|ok&$pVifPZW5V za}S>>^Wce*%^nY~11@nyztz*nY%A*SolTzr@D%S3T$e9~W_8hY??}DOd!~1!c?VA~ zzpkSNv%TZdw|u-CWpnn0u&yas;LUU7nT9Fmq3kR1BBzq{YrGvDdB(h>BTt=obmaPc z7f14zrsoIVFg+c43&E>=_CfsjK$aF?<vpH5X&nv!mp!AMKjP`&(asau@0&C|+ys5% zU*JpAOLYgzogn9#E|f!%@5&xQxdQTdGmi2k$gA{bkCfHuc_(uN<tflVpLRNZ&V+n> z+PRb~eUs3-R+Aig{(X`oPs9gIkD@z$(;Rtc!YoJLlQ0b#egyi}MZbYulKu)%e!)o7 zcjvv!dg?qg%_ZhfzL{oN&WFG!^FHy-VcCEgT=XyB0=9CABkx<d#F6(ctT!@h3!F>6 zV^S74-HJAQ7C86kE=*bLWEHJSS?_ErxD!5-tF?}7YwL})c(=pt!jUcFZbxQyBQ=fG zG*UC0bM-;$AEf?4>K~`(acUl?W*arzsM$u%4r+E#vxAyfsd<%}SE+f6^KB3C($vLh zD@VP>xwi-SWok!6>Qf{2EqT7Qj*h&Apra$d)9i;{wx;z3-P0yH@<xKCW~BEN$nr*l zNshdcV3H&6ADHCG`v)dD^1gzVjAfD|?<kn$$geTmxCL$F_O*@M*EVinqTlNAa9s8f zKV{E9WzRol&%dGO8*09x=6lBSJ^P=9y~{7tvanD2EpVFp3^+q&__MHjcLzrB-is`( z)Mo)NREvNM)KcIgwH|nhx)*r4+6KHry$g(~W`8Q4{rnhslll~RH{~smy{f1FE3miz z8rWa|4LC^u9XM3~6ByS20*==I29DGJ0Z!67-K!>R2RKc8firXpaF$L3MszxGj?M&L zsI!3!bO5+W=K?R$`M}F{A@B;_PBcbZzubK7?}|P2$LZa$-+Q}Vxk^{t^2+fm+fHBy zwI5ijJ_Ghp1M<tUhaC?*QJoH~P!Zrn^&D`j+66pIc?-(1H!A>Mqy`l9W6c$KN6nIg z3LFzGDX64e>B=?gR|VDZSypffaCiDjTI*aC|2kEp_7-fQW|NDe-voVX;j`3i$MM3l zg6(W;JM_mFZpZJ0OA6kkW-opA!Dn~+K5F*CXH4Nf`h0^Ul0E54OAj^C(c+08SdkM) z*Qf=Bj+WYrY3)obrB5k+O5t;5VJUsO=r!={2mP{wFg-^SE9hTA{|fkzDXgIXczRBy zz7qPag_YD-Qy*j5I(>`URai&;E%e+#{U-RoTeyk(Mz;GHOFa(Uo!&&BXNlY4`9<M& zdhTQmZxWkn-OEyYDer@4LD4?8xR3rH(({0BuO2SYMzZ9X8r8GNF_IH6-sSLddK&e) zke3w{Q|?SGHIm6v`jn#7@r9)<)x}7bx>Dbd7^eS7Vg*Z8P+x&kqlzk6Y78|~i4l6v zCRWnFlKM*eSJHnjH7kwud?h{WV4YV~N9!u~VHK@c(eoDIvVu*lZ4)({;D1xmCi*wh zb2IR>^v7szVx8OJ^NXVG^w~-Mo2+La^zRq#hyVTUKBWF@qT_IOIg&SzBUUfvTw<wn z7c$d@`tFWoyC3w+3M!}{P5l_)KhmetzmocS^sJ-2iauA-+Ccp#>KmcolfIeyt<*Q6 zC(8<&C~s%4w$r+Uo;xWw)2Ep}`{=We`VWZ*sCPV!$RiPXiD`Jcdr3hl^`+F5dgg14 zIOQ;{6|`1RQ$gz(%Hx5%)5p`N(z8i<+f~xDlGeGzdGwh_pE_FWsK1KXK>a4_H&L_6 zBU#-{c`I;t`d0ch5x3KGC$0M^?{j78^M{lVu+#ziJ6^`?<@(?iA1~!n?<Uo;T`8=~ z3QE20)t!(THLcyfl9Mp8f;EhxK0-|;HFHtQ$(Tn?omcvN6*Ze^-9-OQ)Nh9Vv-GX> zY$9%_ekb+K)a+xaeJr())(=_R0cduoAD|CjP>NPG(eW`ZALH^#>t0`bm7P&cYbmXz zw3gD^m2!9B?)2{T2@^*`gBeTz3i?#ge++8-EImS>N?I#vt)z7>t@BvhJo?1ov#cOS zxsLvI^sl4;Rg@cO-9+mqYBtfjnetX3#+p7&#Am75PXF!n*-rnRl$+_ZkJf$E?4$KV z$_IeR8+~+&$W96~1{r&b6lN?%Vl1V;l=@QYyHf7XQr%M|=Of|iWK__zg4POpj-gyh zeWfc)&2uTw1MW_r#~NbrSyr%;_0-Y7jy`p)=PJsZsNdwuQqN|}TY<<7>)B49?bL5) z*`1X468F)%&y}Ty4=Eo2A{(qhr!oVn%!Vs78<b0_FLh<ncct7Nh?$eh?SeiPE=p|` zsp2_?p0nvwNoyrFm9);KJdgFvqfd;ul2}LoI{MVn|0>E2v~HrjiTcfyw*rw9TAPT^ zrpjn;r@WIs&6M|1-p8^ZQa-@42WVAk5|NX}oTM=il#8h^rCdsVSIXUi$OEkvX)>l2 z)Q=%nQd3EJE^!_ZbAbMJ)YP%It0*_ne-q_R)NiJ|6^Q6qYCARCY28VAA4+|k{vqW9 z^gKXM2QR3`s_y5k_RIK|Qqz@KK}`kaF~kTpl`cxzxzx`CqR)QGOpI7Z{Z+)(w63PU zfw+nK&BP{Zwo~3od<nQa{Uz#~iTkMkkf_p`|8(}8SWHbR<*w-xO9kahVjZy|T{6Fk z@^<1rqRwD#cuUpC>76Od8>>D}596&%yVFNfQ$bCH@@!%yH8IL7iFMR8P~Je?gc?3h z-;^OUq=}kmiQ9?I#J$9QM3pH$(L{&XnOI63Nvz0}J!VBFXHll~vm#UWL9=PCB*ti6 zNx6==3ZBafHc;L~e3rN!o*$?0rM!=|=`3a=OKL98k_>dFrj&9i^`%)7QJDIX#0p}B zIGY$Ft|T@Pn~2Rsl`XBrvL%Xej>wT5kt392lw*_|C^t}UA~qB8MsJkb5Rf&tnX(E> z4Js&lPf)Vt3Cht|G4;jNcMfu7DUYN)nx3QKxhFkB&j_uvX^l}6qh=*F4b(JH(?o10 z?xl4<TU5Ey2c0Wzd2*!>ohf&rJd*Ng%BzTtz&+_rEVYAjbFReFOwC@R%42kSteIF$ z?3~AlXbn>np&X$cqa35WlAaBen}|E0LB{fBG-Jf(e5pT-Gd7F}F;*b$#wa&XZlK(V z^Rtfa8VlO1C)zbp(@bkK<^8nohjnK=RVcN23T2BfE|l7esR<J!#3o`hQ5CTd#0W7) zTva4BG*E7&+(fy#h`FMy+Oe&65?3+hFfl@$Lw&5B_%}k{oxX$E++OB+b9*VPI*1&0 zQ8W?Ckq#oqhz-OhVlz>76l*atOpFlKv8<Wc)a3ycD_9khnYk(?Bi<O2K6EJ)O_ws! zgv;1^nbbDAOso;gF=7L)4V0TGH&JdT;{PO)J`lsi2r))%AT|-3iN!sot>T_iDol(J zW7NkeH&AY%+(c|9s$Oi17$%PHCG%l)FB$O&HFJ7N|6`OJh)u-(<>I-&Ts&jPOCMs# zOKlCrCSo)7&6HJd@ln0mA~8&i5M#tuy`|1oy`|0uYMO}6MAe67i4kIq*g$L|HWO7} znH#Dv{V5ky?$TH4@6uQ54^tDNH9|Q?IYzmGas%ZiVlz?olRCr12r))%AT|+q^pjS0 z^pjSa`*BuNzrUYkQuSxw`m=AuFg0Py5y}zDG0HK@4a6p5GqHF8dqRv5=M0c`=M3P8 zP_t@)wAetoX@IobM7f!AGi5c9JsBw0V#;C4VagH85y~-Q1F?yyPT-iHAT@YSkk&mX zNbAMabU8s<4^xiNGeS8=IYzmG*hFk5szK}zF-(jQW5fnx;~;6PagemNW017cJV<)d zOsg8q8i?V+q7PG!P>xWJ5gUk2#Ac!z!m`9Lar6*rYxEFlD?-hjA<|-ua?=p0zlm}) zQJu&dP85AH<uK(i<p?oGY#=rfn~D2Rl(zPtC~c{sQjce-v{g(wOpFj4hD!Ynl$(gn zL^VwGY8d-KxtMa87$L@ptA<HitA<Hi4b(IaleU^DHxt!x)<X;vW5cEX807|H6S0~4 zX3A;=`!IrKiCsoWTU|y-TVZNOkC3(^lw-sOViU2MsKTs)7zs<A5y~-Q1F?zvCdxa) z(#npow9-s{Gxht!(v}*@+K6Fdgcu_>5Sxh2L^VqM!=t3#Fy#nw&M0YR&M0XmM$M{G zQhx(I8>nfb+(fyVs4CbxF-(jQW5fnx6S0}-87-}NMoTNjqoohUqosx}qou7d<p?oG zYm9OO<p#=4#Ac!z!`6vmVuTnYHV_-fNQ;eQq{Sv`c8p=(PU2{u#2ScUVubn#<rw7{ z<pyFCv6-mGvMe!796eUr8a-Cpicm9Wth5!Q+(2v|EA2K@R^wO$F+7eVLODV?LODik zAT|-3iTlS%Tl>dJTWY-2;~6h)6;loqBg7c7X}r|mM7f!$Cb0DhqA#W#rW~dmA;yTS zCP-VWCP-Ti)HF_zwwfq66V*i4Mhp`p#D<Ade*@(vVlz=q61|$lQk07+cbOz@b(th> zg{c`mN!p4~ju9J(O~huRI+^tl!^GIhQh$tc1F?y?<7BCS$H`KEGkuz=-+!{SqE2D2 zPGN1tFfl@m5gUk2#Ac#8Rs21t@~nh%7s{iFkyEA42=y^y1F?zvCd$o}n<=ZwY@HY; zMu>AJ%aPce$#TRVqo#q_NPQ#pI7+8x$7G46nX;N9wS}ihAHtL)#2B%G`Uc8Pl$$6w z6V++#2{BBJ5M#tur}0?vG-<bin#R*OD=0S;i>FGv#Z%b_VuTo@K1R8Las%ZiVlz=q zW9!5&(|8m*jYpx>jGo3>K{-ZjAT|-zbdKY6)<6ssBh*JI$0)}rHxQeM%|vxN%M!!H z2r))%AU2&YtuzzW86wAsO=o<9oHrATXNVjoMu;(DgVoQJQsJ4>R*Z6tas%ZC%1y*( zqB@hkB8G_(VvN{eHD^hk4K9j*(^-PeL^VsSY8LCETueDkj1XhQ24WMjnW)ZYDPouy zJDX8cZXh-ho2hT6EHBMQi|4Q`F-(jQW5fnx6S0}7&ZR$5?oLwb7<}ZrtNY#4{qE&{ zm%HD`yWhRt?>_E#U-!G8``zFD9^iftbiYq<zX!SBgWc~T?)QoA_fYqHnEO54{T|_d zhu!ay?zg;LN%~*mevfv)<yAbQAM1XPbHB&C-;><$lilxA-0!*W_hL1{X>|VP{L9Jm zggiq$%RR>H^A7Y@c%Sk9-us0&)7Q(_$2Zuw)|Z)ba!O;$uTx$~c`ap6$|or*H9xg; z>d4d?sVh>yOFb=ZX4>4eOVaAnZcS^%yZ1HzCoM;%;(t9#!yQ$ANC8Me{63qjGE_eQ z=b{|k{S&}1n7R0+HXnCV72-~+cDR$OgDS>&l|t$QsXKoAoB%cvV>cQ1QcZ#8H2j`B z757n1!#z~fao^DCsvquJ8h~HBPrzMEgVmYpM0FNk&^8PI>&w|{B<@<Oz+FpY@C*1@ z+_f}borl^gaWB*ybt>*qnu7b1rs58zY4{~v9)X{cb}pW}859ug;1}HNuLOQb{eOtR z%()PnSMnDCyLrW@e}P~PHQTerYUPWHME^?uV&Jj@si%MLrNDhb!T!0+C=0fwx21k& zR>0@TvbBEUsf&ABO>fV&aZQ!yhPYheNz{3dXMJ41Gf95Wb5~sduxCSDp5ggvT)r+z z_9e*$N%AdD!hds;JUB_7m?TG&<dsQsbCUc`lKfPX{A!ZiElEC`^0Y$9&)UKV<Na^R z(j#$AmFLMkgISAPIWTX%n9qKo!Q4#&W<h6EHfFQI49)}w!8B%aHsk{63}$dH@L0@j zjhUPexdb!ZU>3In9*0?OaL>Xqz#gz0+!5FrI0Q2t&+Wm>V2*YJPQrYb2k3hMXJO78 z%+}+97h&ET+^>QEs{;4H4FFz?*>BWp%zvZqKph76!wmyIjIF@n-aYyM$)3TMfM*JU z2LDUd7~pAoEO4qG5B)TtQM2_#;CcFFXexmQ_gtI`tkqMXSq?O6jh+fztEWSAJ<#BP zkUIl-m!1jD-9V$Z>a&3N>$9PG0BF=x`dr{I^=xQ<1vKgfT?u?q&xPhCpi#SV-yr&? z=Rxxh(5MgfMZk~r#n5~VH0m221^!JhhUQzKQI4qwddw1Nyg-bIxeS<VmO_&UG^(?y z0hX9rXi9-b^)@SjeauQ|`T~s_f#1T73Y)8-83{D%6mt#mRI?hI$v}hu`)&>J9CJN1 z=K_sdU~U9nY}P@u5NK45xdnIy-r%h9bo8x|uQRs;*O+=})&em)<}To!<{oJ70vh}; zh4%utnhnt04>amovkAD}YzDq;ehPfgYz4k=9)QoEfChKt{S5e-c^I0{ffuOd-d@0% z_jury_<wUQz?+!+0k8HB0AA}o0eGEvFy>cZ%$6`##Z$13&cd2FUsYi?#W0s{!tB|o z9#zlcF3#80yXph=mHG~Mr}}lS?xefv-g>aU0{5z|)$8@$x=}x<AJ^OT4*jZrOYhP9 z^{4t9{k`^>43lR%nl7fN8DNH)G3GQg(?rY#2G8J|T62|IYi==jnhj>FdDJ{*o-w=4 z$Nx`zS05Y4aouO{o1#SXj#gqPq<n?l#;j$2Np|E!QB3hex<r&@e%NXt_q4oQ^42=u zo%fa!=_tt^ptwq*0CroU0jd^Cs}xF_6i(x`F5nb4kOU2yy7>f>KLWG`>OTUsg^T{t zv}xSm@6GJ(Qb*Y?g8oq;C*97>dvD&ndGqGYo84Jn?DbeC-XDK^{GRx}_<Q3Yi+?u$ zO8oWsKq8;GKk@#=sl=BO-%I>wqA!Uj(#gxoFD1X8{9f|ElF3vh<)!{O^;+t#^l<vU z>BH%#($A#7oBqf2yD}r01DPY4lNmenMCPuZvprAsT<%G<;OsG@!_Shu3&{0Uf1c~P z3-9<UHH~>XgIPL@?-8s?M=@6moptGM%+b5Cn!F2Iyca9VFxIJIvrY|TWf{iGG7S9= zV|5wEIyH=SY8dO(FxIJI%<N&F&%!!2jCE=l>(nsTsbQ>B!&s+=F($)Ur-pI5b{J=9 z_hRG@;q=x~{4!nvqjd~^U=HWFj^o$49GtCv0_)fl=A<ph@<~X42Tp|j72Z9*9km|E zn}!N<`~Yt@-vK$8CLqTTbNns652+su{In^bma@Wmuy?<m_zU=@E7t$0DSy<IFPid2 zQ+^tMg_2r)8o#R)L%pX}U!1?l^)!BAi|u|>+Eq9|togoT>V4XxJpJnLBvtC$7LG50 z&$mtayGS2b--ti1-kEq@J%I0##7ERqI3e-1#LH?d`FrZU$!pfvQ2uf9Zlw3&`#vfE z37!o<mwv_ia{3y+uUIc5eHrN(=*N<;SnteSldtOeU2D4MK7oG@za&I2v<3}*8s9ZU zGOxn_uVGzy73;yPu$@=27Q707|0-614_E`B8vuSc%5TSs?>jJS-hmXKgHzRd%{g?B zI(X=zVsT$_OdUMp_><LDXW6se(&A>rIkapLhf2lbwA*Y{?aisG-E0c=&A^HM<Z&}} zTi^piiDn;UX4b2(u_-BRCj;4vjN`YG5r}!mE*~lvi{t9}dbMgVSDiECsxVbwYuH{; znyIa?IbrF^b=wb6tp1qi`mD8RudX^}5~D!GiCT4Yp<&m^aR#-juD_`{JD$KaU%$@M zF}LX>nO>f%*L-J#`PqFFs(|kjzT~%5sQD(Va|cWH^_qX^fnsq&&92u<XC_oc)(Q2f zU0rt;*Bj`RDV%8dZoP)mytC5Df6&<!%9FP10VR@5s7cSWH&L8+ORQsiEF5&%8h&}~ z!TqX$?-IU&XeJBMkc-p;<@keY3EzOyWI>5sVd4Q`d5MVoZq%7LfJ(TmI(~6!arS^J z0B|5mb$}zR4$jrf>s9BFg04%B@2)x1ho#a&;X^Y~;K<>H$;IQ4qT%V>!e-NV)<y~^ z;Mt_{`Nz~b>I`Y2R&zY0u1ROA&YDy60dYDxC=TVba;20Qu%c=P8A3388X_6#NdUp9 z7DY_x($r!`Y6TFeSVN#^O(Pkh8Gq1lW=1#%5UASlfmRKX<b<jNqs|Q-)EqElPIC)j z90WrbPz5xmyo6NrR@8Kh7z52u=g1ERr$Le-ME_i}1_pV;VHS=Dc1EI3iMYe{x`FGQ z@L9*-#G%#~m?31&ZfJ;P2s!D&MoPYhb_&FZnQOSo5<0ys9Ffot7yR<F*rH_Mtdy~w zsd}~Q$n<EA9C2VUZb<}~cJ0+#z3IE9LRpog1u~yn_dJ-=oL#fwFcvEw<~HaK9;z0L zZq4;wyV?PA%Bab5SrwYo&Wc-e@C!9`CC#PUIka5&+@}CKNXt8PwpcuDm(JohsApZL z3Mh@P8%o3LhSCbVp)|m5D2=ciidnu5l*YIXEp4wGEGF3v<``^8y*;?FE$wzdvCi)1 zXrbLuW1`)Nj=grH#7MirG}LY=jkOyZ7;861n`<|GdU+dI#0I;ub564fQ``>WSL)^C z_S!bMS;zL*J!gC5Nvxi04(9Rp=)>!7wd{BjEp%D!yaT>9@yFc~6`Fe4va8*=C2t$x zoU^v<c+GC=NpBU)dd=Sk1xB-VZ_W0*Yb|Wne0!q{KG|qEwX&)?e$%&WW!o#O#X7tN z7n>?pB{hXf?mLAV(I_5q{G)cWGF2}-+GFVvP-`ZQL?DPAI`M~c!fb(|RlUhbL>AWR z`nqHh5y1UYwMQWdkZ6k~#!Ci5r?LPy0c1hK1CfD4WrPVv9rARt^jwiKS1aN8ORX(K zt{6Ub3_zXtY$(WsE#vBMN=r31#~IkYM32ntPEhnMp{25;fu?HKbw^E1fKG}e5$jUR zp`}4fs|d2R*(smYK>!e{aztkxvUJNBGBs0n;bsqe_4A_Z1)so!hsKLVj3u%KI0h^& z>z-YuUL_h-lg%dVuDUtr_%^uMlfLh{%j;wZb1avgatL8a=vE!iD0kj@a^3Z|Bajsa zO-Aa@Y?P|&Wf@!A(p3E%0@YO~WZuawafWwFYT8*|UtOi~gs>n24k0)7JwyckTXg*> zhXS-~LtCPxIlENBf)!Sx6^5myTC)UmD8t)^z(w1Goz2pA&ey%OA!23&mIGtK?5l=Q zJ-Lt9eTp7}b>|%Ks0&TkLiiT)$0|`&(4kq3#G~*Rh<L+#0kTkA5x2l_xY|WEWg{5y z0=EKzJxt~;e9X``kJU>yW9se}5d5#WtFWc*(bG<|<hc^Vhjjyk(Y*`dVQ^w5M2M0l zt5L7gnDn})J%bmL=%Biw!x01-5fnyj!CrCv&3S~~&9I8N5)&tLibqgE&rf>JIoCOF zOg%!NCxLW(-l^Ifl8fLM;90kJG=dDq$E=m1L;+%|-q`fq)rudY7S6g2jzhJ{K8EmH ztH!JsA!#sQ%#JNYXqD=vB7};Altnv)70@ZYQ4C?B(Ab35HbaE*in4|D-DQ{i$q1HC z{qQDS3|UHMu`UTcw=AdP9V5%D^1`K7a9EQx;W_St5OW@PRxuLi;3;7OV7#J=MXo{E z>#Er*E;F@Ky^N(KaHU4Z5i>i8;X>#!18;6}b8gA2H=R<wR&HV;DmgQada0t8YVL-~ zd9JE2KLPel#25?fD=Tiv#pIvaa7yc(8}KkC+%j_3>fYv@?KLZQRUsH&bulxgD`B7@ zk$j{W*(Q$2XtPQX#9_aucOOBAsBPTwK)4S7T2+(P>aEwSkutES)~bqepGbj7LYil) zg@pzjd(CiH^H`$mYl>^LLR2W*cUk21AcBo!*HpbxJc3$Wx+=PO46a4;h2}BWhYy~q zVVhA?Tu*t+K^gt4vhgKBcQAxz$D-!!vyKj?6g=p{dZSVI;195*IO1V&2S7(iC)WKF zE0A>632_VOUB6Tbom_}1)L=Yr`2_s6OyUp|@CzXyXx3{=?Adb`>(mfpHF{YH*9b&D z=GLA>oPxzeFW{c-!Em{hGq)yn)L{&~w@J%)Jgm<9ibW`ld2MmV0|wy$$X7Yyt01WR z6<lSzuZN70isHm+KBmJKRfKK~Oy;kV7*x`#EcqUS9NY&Y%!flbhd2iXPQ7FG^T-ve zk_5pV7RjK0p`Y$-B1Zx;tj`CHM?Hicrf)*QThxx!x`}uao78h$nbin4jkrn@m<w-- zsD)C+;f<6@Uy9SPH{wsMtYCAkDfIo4<GdZ_mYP8a!rg9D%vN@vO(kkE`RIfXss*k> zre3|VfK9(!ayncCG?mTpsDsz5I~)pUB!0rw@nx~;xV<Jkjtn27Fr9)#XO82>n$9hA zd^`92=*pVfYz`jUUo83+w<AZScO#6kCgCnNo51Tn)mQwjU=I1BTRPk1wq0j9y>(9S z-2)9oMqVsM+dq}4L%^VX67JIv3fka7q+{fxP8GH(jWOOp-4&QT0#bm2pi@UxO~kYw zoCGkGF_?`_Wd}Q<hzUS7GRS%#q>ik+Ws?o;!W6|nHevauOxB(KQ{ejyj@E0kf#pSt zyj^OFY$H-6Ptly*K&@fkmwXKJiRCA73q&p8h8z;J_#?8#vI5@#6>OvD&dkCr7@2oq z*(&IeHgr{WR(vJF0{w1btx?6&<6|wH*}#m(;4@y}Qss*paaYD(ullq#@hNI@brs$T z9We8jt}MvT)XuqHy~Z%MqxJ2CH20FC!iH|Z+&<c_Uf=-Oxi=ttL-thm8`O<95hXo% zXrfpw={sAZm?_?yhI1EJh|%J0Wi^Kl3MXM8{37%8+Vl2#-1~kMv7z$LOMM+asithW zL9^;?h*uU541PMrEm+=w<xm9{BEwe06v8s(Z>j>e06sG_YYklS*}aITQmgpq;EB`> zo;I!HS%*sJ)rav+OpQ+BdDE)E_$0~)Yz?KR<d#t<kJ@=WwHj9w3g=^b&!Joe1<s<p z<+>Ai(v7n=Z;_s7L!_;@OmFzd?^l+kb}!)RTSa=i4oMw6LHCs!n8tItDLiLmA00e_ z2dtN{Jf5~g4?Rub_%u%SAOZmua_^i(ed=u)I<oOpsf0V{n|MwK+y@`+=z*FXQy&2r z7kbO1_PS_~e?2EJHEY6!7(%FLjsfe9qn^5}i06kkMyCvF2Rxc6FQX+&+EnV7h<Ab2 zBjf_Uxs{@45qOb4xfgs@HYgWSlKy22rsp31wh>2A-FY*lLz|vQOZXG!x9K#5FNoag zHl23h_>ma|7Lacp8iHQ&Gc(b0hpv~lMoq#l2f};@pPmD*fp&G;f$v%XR}Vd;9yz{Y zsYBCq7!uUkuWXR(;F1Me?k-+K*pk3_?ZDEsa@gr@It}5pNWyg*!V5UZfiq^HXBxtn z3|yxnyZ|oyg`Q~$Uovo=cHljoS%=Znb0WlbW=S^4g}4Hwkr{`2Iv3)WI=CR$!KG)7 z+&YcIwcN8JX9w=mg0wZ!I`bG8dYZg}(MQl4eauEZGw4-peCbu1fO4i(rKa{Y>)^*3 zV9N})F$Xw*amw%hIdCWeS4TYuInK~B@(W@WCBSB2Id!S0?z|PX8rse`(8?R&t$L25 zWtm0k4*~`&?G0x+JgG|U`i*D&f197Va^Lg$&;9HZ6DpCntV}$wEd0o-mCdr)m(0hk zK3oBqk?XJR*qw8%-MKT?fJ!l&EhG7ePEe}o5)+iFy2J#fhAuVIyK}A2TUlSAxec9M z(9j%H$I7)@8KLTAvaO36l5Jh0Btw0-QHtH!*6+u%t!J~XE2iRQown~a>E3|R<!FVA zU}FewnDj-HzGN!2Z}d_Wi17?M3uNmv)=)AK;%bn&cG$@A*-U0A+q%kcu5~@zx|a2` zKIHpshMDjmKwn6QMdozNt?NdDhfO+d(xWClX3~=;T{P(_lb$x|8IzVxS}|$Wq#Ob0 zxNX{PZ<w;teY<7K7fl*yDp2h+fl4VbhH{UkQyi?6GJ5!BZ683!Fuq8VI!WuKM<>0y zUY3H<BxoT>6FN!hB%_lZhK8LYElmvRPMz#BE$3Q)iI!;~xkq5jF%rKFNFtA85daJl zK$4!kGN@!;<yzm+T^iB@{do+u=G)id`+ZdElO}QbVQfGR1iiLE%K4C1Xf)gU=S14d zJqZzVPjcqOOg@g*`)*UQm?qZO9^g%7aD5H+aoR_6B698#FvyT|ES7sD_Xs#=(f@4g zC!BDYo3~k+{(g(I;3rm6u6tBshs;I+^()}o7K9VzfQqGJz4^EXCS$QA7`EE+w6%lX zea-4k=VR=eVoNX^X{kbvv>^~=tHfZu1XCqOATTL`Gze)VmDM+tg!Aa@>tnr7#*#6* zlEFI$`YmbzLx6LMMC?(q_zo*con7Ft4JF}tVuMiDjzkI*JC%oo&*JapA-S>x;0$zT z91vM^ZpKp>@_q|P9nrcTeE@MR8C{Wa7|e>^h1k^DA)4<@?bpaAZDrEwL<W;Q*@Nyp zAM4Au6X`rqxz@)skU!V@1l%548rq|h=xFOI<d#;^jXi2dI-YBN5k#;aT3~w!W{13t z-e5|_`}zjiGeQP2w$#~k;Ev9T&f$6o>2`t0wLb-z822Y*x%R{GD4YiZ&A_x;*8zlo zf(A7<m5vRdhL)Sg38kR_76ilvOk<{6>r+$`MjA-8&xy)PFBpr~5Du~rNz<&EMy7|E z9%Oo%!Jq2@gESoPx3X2Sp>F||AS!`OiW#|Zz;?2<6PaDi?CL=;u1aHU_CF*C>kG1p z6m-2S9Y@zVZ`qSxSU9IAZH9GzDs^_|>6$r}Spg&%6GOC150yc~0yv^d<=XcnFNO(7 zA6T_B!Ya!tlxuxo+a#OGrp3|gX`UH~4a6a~nf1F+Z)bvn5e_wD#>9}sQNr}0&{jU3 zfuKJU?kuD551j(5^}_uG1niS0)lf}IsL6q8|A6WrusF!5%mIw?5*H)9h!M!OuSx!r z<gd$^JWHiv%C?@B;+3Fy<@PjY7FWPr>vFDrBiDX0*M12=Sgu}51$b|+y;rz>UN~Qq z{Bzn*WT=kUEUtB$D-2Q^xfV2X&G-vHla6sYY#*dXp0kkYMdqilovYdQei$WuE=I7C z{?%=k&#lYk))MW$J)LVG&9#r^+9z}E#a#PTu6;V!K9g&gbL~p5UCp%{Smtu=ja>Ue zuHDMDFXq~pa&7ebnI3H<Xk9w?JUR(EF%^gUa*v2R{3Pao>#8`QtFRW%H3HtzA9yfr zfE`D+$4c!GidZTG?~_TT<Cy^~)0+mJF?rFE1Tq{K4je`yP}wD_s(mBdej!lR3w`N? z87Ubfk@T-AY5PXzn;#x{{crF8(IdC(osHf}=v_-w?IG|<gru$np=BJ=bPx_QH1!Z_ z?&0onO`^;uNEpS=0)l(864MS&o6z<ILR0*SzU`60zARev_Fz-K1$aaRDQ{tu`2gf# z==+Z2z%o+huBfzyW(Rx0P3wjl3|iY(qyV{qF4^E2Z9TOloKvUJNL3~9(;$onusSFU zUeStM2`g!(EI5Cw2WN|Wt)12`s}D<Szm>IeaFw^mQ$xvE3jh1!DTEcNA*4ftu~ct7 zh1Cyp>38V0t|Ql0$hGf~kcEpLk>BLy3XxwGKc|-!mQfAUA(`SdBGEPQ^AQ@Ec|y?N zV+jan3+WH=($5Az#oP;|y2#6=PEEqvd0y^GqL&JP$rs17*<+;uz{db0I1=FReZp=* z2b!H0&YMae1%?{ANBX#$$(A72M@Vc)lD;7<T54!8!-$G;5LfzMt`nI-_}dI37A%0- za<-hrii~SsA?83nTh0Iv2)$;u3RMDAl`0jfQpr}NN=3+c%^>t3GY|_*Q#S<GK-@ug za?NcZ8XHQ&vB*l?`b3YS8-mjMNSxcqZJ@Q>25MN?f*^9cfXD-D#05cLkYFUHLlVNc zBN<A93AnWIA6%K!%q5|lWzQI~fLhwcxC`t`h72wP8Dy?#R+s64!HSB*I0su9&HZv# zYji7uj1ZIDH3m)VAeJEz!WnuaX4yWL3n(=<lpIR3KTN5;p=5T9ammBt%yi&D<@Z4~ z+8;tg*F{EVw2aKngfjjd+Z^4k>(iZX{d~NjiR&3G-Fvux^uQ|*CHLSDRHqpWAjbef zM>9YNA%WSM$mo5XUB{XigIu%}Q)KLx4vT3Nd%|K5ioIbG0W(Y&<&5S9`vjm70W?q{ z5mrc`$PmXgmO?QT7BeXB2#Y(&tc-HN9a9CNcmz;Eg`HuAoha@Ki@QKBlfcxMxD<mO zwBDzKC~j>440Oau2zwoguXF?i<ZZftFdVb@fgnEMfn+|K%3TNnxeIUrv7`i@m@pqV z%Pr;%K=4>02n7Kl8VRVlD2j<=#`JK`wGoWmi@q_31SWhh+OzsOqYR;*QSH5mS~MXO z5biaES3t<w)@cL<X&1Zou-&Tyx)udp10T$`j1~kj$HSUG6V4^Nkp2u?p&w7d6(YeV z1$87R@*_@}*43Dx+B6!n`a=RlF-J846A&DQ=b(WLqhK;J(mAHdn1Jk<bPsqIv4n`f zxu$EOsD(ywbW+!1f?6kyQb3McJ{>Oy5F%p%5`hs+E$S*vP(_md0F6NFltwcF`joDQ z=<BqGGXeaxh9e|9qv1>dKcnG@)5;po1n{zkBY3N5I1|7t8jgsrs^LrkuWC5LJWe#& z2NS>>8jje{*Kj6)o9-bX+|Y0)fNyAeM2QzPoC)9;G#sI0OT(D}-qLWylNU9d3E&qs z96{zK4QB%QB@IX9d0E4m0Df7+pFu>b;Y<L321AHZ4Gj?mO?rdTIl3BIGZ@S3+BdeY z83qA<H^>%KP3+@hU>`=>7qJD<^_hVBMPnb*DKM8>ojGax@)C9px)u}EddbuxA%*PB zD-`1QD!kvT5I)_jV#o}!G&G1(FEZIGG8tqLJ)nf8l-DaMkYF;QfZhw}8i{=n4@&W( zTrXwt^AZeJ;QbNOEXLl($N$Icwa#$`9LmNaKty;F2Pt*LQg@CVpIO9N6gl#_*BrWd z-?@pAad7Cny>pPwyavMu)mc`_2X*ow#os;e!)rCAvpn*aKV~oE2?rvlG!8;JcGJOa zScFj%V-vVhZd>Z~V#Ucv&amX=U531Q9GrLYz*62+XyzwR7Ml5$x@RbwcxaDDVo%Ab zj8Qx~kCyXpEkAWPZY=MD7RE-#Cq~9b#_%q$rABq>zKJnsdEfrh_}D|G73Y9`;GqNi zorg-x&dT`s0ej!XLt|qP?6dbfc+(uWHhLohibC&<0Q7qpokO1L{g&E=^96N0MaF3! zJUrf8@%={gJ)@(mu3uSS9x2t=MoWQ$@RCk*G{OymxCu!Se2(C!Eh9M7k~uYj!xU#G z?u)$g6GX5UYW|9)E}rF2=q<)};gC=L{2ZR@y3i#6J%t+1h~OD_nPkUqe@kSHmEVlp zZcDu`zqNUD8??l?P<dy9@%YV^U;o&4m93k}_{QLywE<g~P+vjPhe6-n*k$0~j0-BW zLpN0h^j}h4P$>8%BUnW3HxG&hvb-UCli`AoeNzG1=v&Y@$OhkBHuW~`AQC*|kdf0Z zI1#1r-Z5U3Rw|AY8gZQUN}B@<NqLYR$0>o7KC6`A>9JVhKy2wi9Pd=b@dia4Z*s)( z-bNhnW5n_HMS^ccB=}-Zg0D{{_})Yu?|{Vd7RT$`eej5lf69O#>{pqX%^R)Mpzten z8fD6|wIwNxAo{aAfAE7RzNoCZldSq@_@0KpJZ;Q`<YYg9@4N8ry9>Teb45vixOa|z z<`<?H{^pOKANlHzFBCrW=fCyae{rSvGvqN^!SU`<xWMu#9b@PL?uRR|tCe!p3*d+h z&Nn%u4IF}ZOQR*6J#(6)%Px+^)mEGLmvBJPYmADk3>?oW4j7IaA3GX3xet#V<-FBf zjWqBo(8AG4C|nt#djg@MnDIaT^?(0PJ(c*Ud;a??zxg;iAAHU;pASk~Q#V4VRB@{A zO;@XPwp&wcI3n#ij+`qrbzc8vu(?@<TlxK;m-r4X-+xuDtxaiXfn}o$PP(J`p2}Nl z1x_pYtR%3gsYlfUp8Y<GCzkWb74Yuyag+<V&LYpxR}%mFGg=mX7C%ACzca!QS<)V( zpJ@~<aJ6ygW)^pa_~ew&XIF5;h@X3;)*`UH+0z7`PhxTI6eaz6F7b6f0Dw&4^Yv~{ ze<(C#IPswW?Z*u==s<l3??PL0!eUMCO!1rrR(712;8~13xbij=pVYEd-n*JshXFqf z%p|CeBR`M3TCAt>JWs*<ZR`o@HOD-tPoif$X~DjEH}T-}Vcr$v83$KA73t+Do*45y z#jWscuLf>YcrP5G0X~Q1??VLl{iwH1$Z4Jt13!f}_{{mNIxmu(MeSq2<8?Zze;V%* zTb<FEme%<c+eNRUy5C+mBN5$<+@u?Yr}#Y)-j7PJcwaIY&oT6YX9h+vI{fl;8jpJ{ zu?pV3^w1w_DeTezUXBsyw2FU-MHKEkq4&J`NUqdc1NvfbS78G_XeJ=X1W!QVQx;iH z7(6x4T7kW>#4%U5u$wp1O(@d=%pCmv2ec8MP6>N^E8Ysfo-)=|?`920^?RT6jOT>* zp?4+deGGr0p#=MMaK6OG_aS`uqeLigD6fEKTyQ)&@(}39)B~t(3l;x<(R*ZkCt*pn uSl))_P31ghI7coV>8KpRI8(pB`1~9iKuk}cdpU5pKL@K{RQ>;_f&T&9vI0Q> literal 0 HcmV?d00001 diff --git a/packages/spacetimedb.bsatn.runtime/0.11.0/lib/netstandard2.1/SpacetimeDB.BSATN.Runtime.dll.meta b/packages/spacetimedb.bsatn.runtime/0.12.0/lib/netstandard2.1/SpacetimeDB.BSATN.Runtime.dll.meta similarity index 100% rename from packages/spacetimedb.bsatn.runtime/0.11.0/lib/netstandard2.1/SpacetimeDB.BSATN.Runtime.dll.meta rename to packages/spacetimedb.bsatn.runtime/0.12.0/lib/netstandard2.1/SpacetimeDB.BSATN.Runtime.dll.meta From cff42fb934bb35526c514a2fa7a5668ed5ced31f Mon Sep 17 00:00:00 2001 From: Ingvar Stepanyan <me@rreverser.com> Date: Fri, 4 Oct 2024 01:25:05 +0100 Subject: [PATCH 23/55] Accept multiple queries in Subscribe (#153) ## Description of Changes Turns out, we're not ready for single Subscribe per query, so bringing back this ability. ## API - [ ] This is an API breaking change to the SDK *If the API is breaking, please state below what will break* ## Requires SpacetimeDB PRs *List any PRs here that are required for this SDK change to work* Co-authored-by: John Detter <4099508+jdetter@users.noreply.github.com> --- src/Event.cs | 6 +++--- src/SpacetimeDBClient.cs | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Event.cs b/src/Event.cs index ff687c0f..b637e698 100644 --- a/src/Event.cs +++ b/src/Event.cs @@ -61,7 +61,7 @@ public SubscriptionBuilder<EventContext> OnError(Action<EventContext> callback) return this; } - public SubscriptionHandle<EventContext> Subscribe(string querySql) => new(conn, Applied, Error, querySql); + public SubscriptionHandle<EventContext> Subscribe(params string[] querySqls) => new(conn, Applied, Error, querySqls); } public interface ISubscriptionHandle @@ -80,10 +80,10 @@ void ISubscriptionHandle.OnApplied(IEventContext ctx) onApplied?.Invoke((EventContext)ctx); } - internal SubscriptionHandle(IDbConnection conn, Action<EventContext>? onApplied, Action<EventContext>? onError, string querySql) + internal SubscriptionHandle(IDbConnection conn, Action<EventContext>? onApplied, Action<EventContext>? onError, string[] querySqls) { this.onApplied = onApplied; - conn.Subscribe(this, querySql); + conn.Subscribe(this, querySqls); } public void Unsubscribe() => throw new NotImplementedException(); diff --git a/src/SpacetimeDBClient.cs b/src/SpacetimeDBClient.cs index 11237718..6aa22f9d 100644 --- a/src/SpacetimeDBClient.cs +++ b/src/SpacetimeDBClient.cs @@ -76,7 +76,7 @@ public DbConnectionBuilder<DbConnection, Reducer> OnDisconnect(Action<DbConnecti public interface IDbConnection { - void Subscribe(ISubscriptionHandle handle, string query); + void Subscribe(ISubscriptionHandle handle, string[] querySqls); } public abstract class DbConnectionBase<DbConnection, Reducer> : IDbConnection @@ -803,7 +803,7 @@ public void InternalCallReducer<T>(T args) )); } - void IDbConnection.Subscribe(ISubscriptionHandle handle, string query) + void IDbConnection.Subscribe(ISubscriptionHandle handle, string[] querySqls) { if (!webSocket.IsConnected) { @@ -817,7 +817,7 @@ void IDbConnection.Subscribe(ISubscriptionHandle handle, string query) new Subscribe { RequestId = id, - QueryStrings = { query } + QueryStrings = querySqls.ToList() } )); } From b79d90c486bc56c55837a4c879585579ea01c374 Mon Sep 17 00:00:00 2001 From: Ingvar Stepanyan <me@rreverser.com> Date: Fri, 4 Oct 2024 01:35:51 +0100 Subject: [PATCH 24/55] Fix UnityNetworkManager (#141) This got broken when we switched to a new DbConnection API. Keep track of all the active connections and update/destroy them from a singleton game object. Fixes #134. Marked as a draft for now, because it's untested and because we're not yet sure that singleton for all connections as requested is the right approach. ## Description of Changes *Describe what has been changed, any new features or bug fixes* ## API - [ ] This is an API breaking change to the SDK *If the API is breaking, please state below what will break* ## Requires SpacetimeDB PRs *List any PRs here that are required for this SDK change to work* --------- Co-authored-by: John Detter <4099508+jdetter@users.noreply.github.com> Co-authored-by: John Detter <no-reply@boppygames.gg> --- .../_Globals/UnityNetworkManager.cs | 21 --------- .../ClientApi/BsatnRowList.cs.meta | 11 +++++ .../ClientApi/CompressableQueryUpdate.cs.meta | 11 +++++ src/SpacetimeDB/ClientApi/QueryUpdate.cs.meta | 11 +++++ src/SpacetimeDB/ClientApi/RowSizeHint.cs.meta | 11 +++++ src/SpacetimeDBClient.cs | 10 ++++- src/SpacetimeDBNetworkManager.cs | 43 +++++++++++++++++++ src/SpacetimeDBNetworkManager.cs.meta | 11 +++++ 8 files changed, 107 insertions(+), 22 deletions(-) delete mode 100644 examples~/quickstart/client/module_bindings/_Globals/UnityNetworkManager.cs create mode 100644 src/SpacetimeDB/ClientApi/BsatnRowList.cs.meta create mode 100644 src/SpacetimeDB/ClientApi/CompressableQueryUpdate.cs.meta create mode 100644 src/SpacetimeDB/ClientApi/QueryUpdate.cs.meta create mode 100644 src/SpacetimeDB/ClientApi/RowSizeHint.cs.meta create mode 100644 src/SpacetimeDBNetworkManager.cs create mode 100644 src/SpacetimeDBNetworkManager.cs.meta diff --git a/examples~/quickstart/client/module_bindings/_Globals/UnityNetworkManager.cs b/examples~/quickstart/client/module_bindings/_Globals/UnityNetworkManager.cs deleted file mode 100644 index 21ab1ef4..00000000 --- a/examples~/quickstart/client/module_bindings/_Globals/UnityNetworkManager.cs +++ /dev/null @@ -1,21 +0,0 @@ -// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE -// WILL NOT BE SAVED. MODIFY TABLES IN RUST INSTEAD. -// <auto-generated /> - -#nullable enable - -using System; -using SpacetimeDB; - -namespace SpacetimeDB.Types -{ - // This class is only used in Unity projects. - // Attach this to a gameobject in your scene to use SpacetimeDB. - #if UNITY_5_3_OR_NEWER - public class UnityNetworkManager : UnityEngine.MonoBehaviour - { - private void OnDestroy() => SpacetimeDBClient.instance.Close(); - private void Update() => SpacetimeDBClient.instance.Update(); - } - #endif -} diff --git a/src/SpacetimeDB/ClientApi/BsatnRowList.cs.meta b/src/SpacetimeDB/ClientApi/BsatnRowList.cs.meta new file mode 100644 index 00000000..95ec13f6 --- /dev/null +++ b/src/SpacetimeDB/ClientApi/BsatnRowList.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3736538171ef3574f914e9e880b67a03 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/SpacetimeDB/ClientApi/CompressableQueryUpdate.cs.meta b/src/SpacetimeDB/ClientApi/CompressableQueryUpdate.cs.meta new file mode 100644 index 00000000..486b1dd8 --- /dev/null +++ b/src/SpacetimeDB/ClientApi/CompressableQueryUpdate.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fa7fc0d9632450b47a0d88960f28e66a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/SpacetimeDB/ClientApi/QueryUpdate.cs.meta b/src/SpacetimeDB/ClientApi/QueryUpdate.cs.meta new file mode 100644 index 00000000..ee9369dd --- /dev/null +++ b/src/SpacetimeDB/ClientApi/QueryUpdate.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d989a072d0ab7664ea89ce118d38d8a5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/SpacetimeDB/ClientApi/RowSizeHint.cs.meta b/src/SpacetimeDB/ClientApi/RowSizeHint.cs.meta new file mode 100644 index 00000000..7b775411 --- /dev/null +++ b/src/SpacetimeDB/ClientApi/RowSizeHint.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a21b01e7a4f857a469aa9181dececc57 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/SpacetimeDBClient.cs b/src/SpacetimeDBClient.cs index 6aa22f9d..0df4c7b3 100644 --- a/src/SpacetimeDBClient.cs +++ b/src/SpacetimeDBClient.cs @@ -34,6 +34,9 @@ public DbConnection Build() throw new InvalidOperationException("Building DbConnection with a null nameOrAddress. Call WithModuleName() first."); } conn.Connect(token, uri, nameOrAddress); +#if UNITY_5_3_OR_NEWER + UnityNetworkManager.ActiveConnections.Add(conn); +#endif return conn; } @@ -76,7 +79,9 @@ public DbConnectionBuilder<DbConnection, Reducer> OnDisconnect(Action<DbConnecti public interface IDbConnection { - void Subscribe(ISubscriptionHandle handle, string[] querySqls); + internal void Subscribe(ISubscriptionHandle handle, string[] querySqls); + void FrameTick(); + void Disconnect(); } public abstract class DbConnectionBase<DbConnection, Reducer> : IDbConnection @@ -146,6 +151,9 @@ protected DbConnectionBase() webSocket = new WebSocket(options); webSocket.OnMessage += OnMessageReceived; webSocket.OnSendError += a => onSendError?.Invoke(a); +#if UNITY_5_3_OR_NEWER + webSocket.OnClose += (a, b) => UnityNetworkManager.ActiveConnections.Remove(this); +#endif networkMessageProcessThread = new Thread(PreProcessMessages); networkMessageProcessThread.Start(); diff --git a/src/SpacetimeDBNetworkManager.cs b/src/SpacetimeDBNetworkManager.cs new file mode 100644 index 00000000..129d87c3 --- /dev/null +++ b/src/SpacetimeDBNetworkManager.cs @@ -0,0 +1,43 @@ +#if UNITY_5_3_OR_NEWER +using System; +using System.Collections.Generic; +using SpacetimeDB; +using UnityEngine; + +namespace SpacetimeDB +{ + // This class is only used in Unity projects. + // Attach this to a gameobject in your scene to use SpacetimeDB. + public class SpacetimeDBNetworkManager : MonoBehaviour + { + private static bool _alreadyInitialized; + + public void Awake() + { + // Ensure that users don't create several UnityNetworkManager instances. + // We're using a global (static) list of active connections and we don't want several instances to walk over it several times. + if (_alreadyInitialized) + { + throw new InvalidOperationException("UnityNetworkManager is a singleton and should only be attached once."); + } + else + { + _alreadyInitialized = true; + } + } + + internal static HashSet<IDbConnection> ActiveConnections = new(); + + private void ForEachConnection(Action<IDbConnection> action) + { + foreach (var conn in ActiveConnections) + { + action(conn); + } + } + + private void Update() => ForEachConnection(conn => conn.FrameTick()); + private void OnDestroy() => ForEachConnection(conn => conn.Disconnect()); + } +} +#endif diff --git a/src/SpacetimeDBNetworkManager.cs.meta b/src/SpacetimeDBNetworkManager.cs.meta new file mode 100644 index 00000000..e8f74142 --- /dev/null +++ b/src/SpacetimeDBNetworkManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e016ef92e52934343857fd26e55140c1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: From 8916c18fe1084fe249a2b69b781bb8f73f9511b5 Mon Sep 17 00:00:00 2001 From: John Detter <4099508+jdetter@users.noreply.github.com> Date: Thu, 3 Oct 2024 19:39:13 -0500 Subject: [PATCH 25/55] Upgrade SDK Version to 0.12 (#156) ## Description of Changes *Describe what has been changed, any new features or bug fixes* - Upgrade version to 0.12. ## API - [ ] This is an API breaking change to the SDK *If the API is breaking, please state below what will break* ## Requires SpacetimeDB PRs *List any PRs here that are required for this SDK change to work* Co-authored-by: John Detter <no-reply@boppygames.gg> --- SpacetimeDB.ClientSDK.csproj | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/SpacetimeDB.ClientSDK.csproj b/SpacetimeDB.ClientSDK.csproj index 006a68bf..eb7773db 100644 --- a/SpacetimeDB.ClientSDK.csproj +++ b/SpacetimeDB.ClientSDK.csproj @@ -15,7 +15,7 @@ <PackageProjectUrl>https://spacetimedb.com</PackageProjectUrl> <PackageIcon>logo.png</PackageIcon> <PackageReadmeFile>README.md</PackageReadmeFile> - <RepositoryUrl>https://github.com/clockworklabs/spacetimedb-csharp-sdk</RepositoryUrl> + <RepositoryUrl>https://github.com/clockworklabs/com.clockworklabs.spacetimedbsdk</RepositoryUrl> <AssemblyVersion>0.12.0</AssemblyVersion> <Version>$(AssemblyVersion)</Version> <DefaultItemExcludes>$(DefaultItemExcludes);*~/**</DefaultItemExcludes> diff --git a/package.json b/package.json index 86f04059..2bf0c6f8 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "com.clockworklabs.spacetimedbsdk", "displayName": "SpacetimeDB SDK", - "version": "0.11.0", + "version": "0.12.0", "description": "The SpacetimeDB Client SDK is a software development kit (SDK) designed to interact with and manipulate SpacetimeDB modules..", "keywords": [], "author": { From a7c720f3a4a1d777ba571155ddbe37a4cef125eb Mon Sep 17 00:00:00 2001 From: John Detter <4099508+jdetter@users.noreply.github.com> Date: Thu, 3 Oct 2024 20:15:37 -0500 Subject: [PATCH 26/55] Use SpacetimeDBNetworkManager everywhere (#157) ## Description of Changes *Describe what has been changed, any new features or bug fixes* ## API - [ ] This is an API breaking change to the SDK *If the API is breaking, please state below what will break* ## Requires SpacetimeDB PRs *List any PRs here that are required for this SDK change to work* Co-authored-by: John Detter <no-reply@boppygames.gg> --- README.md | 4 ++-- src/SpacetimeDBClient.cs | 4 ++-- src/SpacetimeDBNetworkManager.cs | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 18cb6f04..75c5fba0 100644 --- a/README.md +++ b/README.md @@ -26,9 +26,9 @@ Download the [.unitypackage release](https://github.com/clockworklabs/SpacetimeD ## Usage -### UnityNetworkManager +### SpacetimeDBNetworkManager -The Unity SDK for SpacetimeDB requires that there is a `UnityNetworkManager` component attached to a GameObject in the scene. The `UnityNetworkManager` component is responsible for connecting to SpacetimeDB and managing the connection. The `UnityNetworkManager` component is a singleton and there can only be one instance of it in the scene. +The Unity SDK for SpacetimeDB requires that there is a `SpacetimeDBNetworkManager` component attached to a GameObject in the scene. The `UnityNetworkManager` component is responsible for connecting to SpacetimeDB and managing the connection. The `UnityNetworkManager` component is a singleton and there can only be one instance of it in the scene. ### Connecting to SpacetimeDB diff --git a/src/SpacetimeDBClient.cs b/src/SpacetimeDBClient.cs index 0df4c7b3..5ba858f9 100644 --- a/src/SpacetimeDBClient.cs +++ b/src/SpacetimeDBClient.cs @@ -35,7 +35,7 @@ public DbConnection Build() } conn.Connect(token, uri, nameOrAddress); #if UNITY_5_3_OR_NEWER - UnityNetworkManager.ActiveConnections.Add(conn); + SpacetimeDBNetworkManager.ActiveConnections.Add(conn); #endif return conn; } @@ -152,7 +152,7 @@ protected DbConnectionBase() webSocket.OnMessage += OnMessageReceived; webSocket.OnSendError += a => onSendError?.Invoke(a); #if UNITY_5_3_OR_NEWER - webSocket.OnClose += (a, b) => UnityNetworkManager.ActiveConnections.Remove(this); + webSocket.OnClose += (a, b) => SpacetimeDBNetworkManager.ActiveConnections.Remove(this); #endif networkMessageProcessThread = new Thread(PreProcessMessages); diff --git a/src/SpacetimeDBNetworkManager.cs b/src/SpacetimeDBNetworkManager.cs index 129d87c3..ae5e2d4f 100644 --- a/src/SpacetimeDBNetworkManager.cs +++ b/src/SpacetimeDBNetworkManager.cs @@ -14,11 +14,11 @@ public class SpacetimeDBNetworkManager : MonoBehaviour public void Awake() { - // Ensure that users don't create several UnityNetworkManager instances. + // Ensure that users don't create several SpacetimeDBNetworkManager instances. // We're using a global (static) list of active connections and we don't want several instances to walk over it several times. if (_alreadyInitialized) { - throw new InvalidOperationException("UnityNetworkManager is a singleton and should only be attached once."); + throw new InvalidOperationException("SpacetimeDBNetworkManager is a singleton and should only be attached once."); } else { From 704640813a0e916d1d1692bc31a6c548a772a38f Mon Sep 17 00:00:00 2001 From: Jeremie Pelletier <jeremiep@gmail.com> Date: Fri, 4 Oct 2024 00:12:43 -0400 Subject: [PATCH 27/55] Update to tests/examples follow codegen changes (#160) ## Description of Changes *Describe what has been changed, any new features or bug fixes* ## API - [ ] This is an API breaking change to the SDK *If the API is breaking, please state below what will break* ## Requires SpacetimeDB PRs *List any PRs here that are required for this SDK change to work* --- examples~/quickstart/client/Program.cs | 20 +-- .../client/module_bindings/Message.cs | 2 +- .../module_bindings/SendMessageReducer.cs | 2 +- .../client/module_bindings/SetNameReducer.cs | 2 +- .../quickstart/client/module_bindings/User.cs | 2 +- .../_Globals/SpacetimeDBClient.cs | 166 +++++++++--------- examples~/quickstart/server/src/lib.rs | 34 ++-- src/SpacetimeDB/ClientApi/DatabaseUpdate.cs | 1 - src/SpacetimeDB/ClientApi/EnergyQuanta.cs | 1 - src/SpacetimeDB/ClientApi/IdentityToken.cs | 1 - .../ClientApi/InitialSubscription.cs | 1 - src/SpacetimeDB/ClientApi/OneOffTable.cs | 1 - src/SpacetimeDB/ClientApi/Subscribe.cs | 1 - src/SpacetimeDB/ClientApi/TableUpdate.cs | 1 - src/SpacetimeDB/ClientApi/Timestamp.cs | 1 - .../ClientApi/TransactionUpdate.cs | 1 - ...otTests.VerifyAllTablesParsed.verified.txt | 32 ++-- tests~/SnapshotTests.cs | 36 ++-- 18 files changed, 150 insertions(+), 155 deletions(-) diff --git a/examples~/quickstart/client/Program.cs b/examples~/quickstart/client/Program.cs index 29ad6c95..8327d976 100644 --- a/examples~/quickstart/client/Program.cs +++ b/examples~/quickstart/client/Program.cs @@ -31,13 +31,13 @@ void Main() .OnDisconnect(OnDisconnect) .Build(); - conn.RemoteTables.User.OnInsert += User_OnInsert; - conn.RemoteTables.User.OnUpdate += User_OnUpdate; + conn.Db.User.OnInsert += User_OnInsert; + conn.Db.User.OnUpdate += User_OnUpdate; - conn.RemoteTables.Message.OnInsert += Message_OnInsert; + conn.Db.Message.OnInsert += Message_OnInsert; - conn.RemoteReducers.OnSetName += Reducer_OnSetNameEvent; - conn.RemoteReducers.OnSendMessage += Reducer_OnSendMessageEvent; + conn.Reducers.OnSetName += Reducer_OnSetNameEvent; + conn.Reducers.OnSendMessage += Reducer_OnSendMessageEvent; #pragma warning disable CS0612 // Using obsolete API conn.onUnhandledReducerError += onUnhandledReducerError; @@ -88,7 +88,7 @@ void User_OnUpdate(EventContext ctx, User oldValue, User newValue) void PrintMessage(RemoteTables tables, Message message) { - var sender = tables.User.FindByIdentity(message.Sender); + var sender = tables.User.Identity.Find(message.Sender); var senderName = "unknown"; if (sender != null) { @@ -100,7 +100,7 @@ void PrintMessage(RemoteTables tables, Message message) void Message_OnInsert(EventContext ctx, Message insertedValue) { - if (ctx.Reducer is not Event<Reducer>.SubscribeApplied) + if (ctx.Event is not Event<Reducer>.SubscribeApplied) { PrintMessage(ctx.Db, insertedValue); } @@ -108,7 +108,7 @@ void Message_OnInsert(EventContext ctx, Message insertedValue) void Reducer_OnSetNameEvent(EventContext ctx, string name) { - if (ctx.Reducer is Event<Reducer>.Reducer reducer) + if (ctx.Event is Event<Reducer>.Reducer reducer) { var e = reducer.ReducerEvent; if (e.CallerIdentity == local_identity && e.Status is Status.Failed(var error)) @@ -120,7 +120,7 @@ void Reducer_OnSetNameEvent(EventContext ctx, string name) void Reducer_OnSendMessageEvent(EventContext ctx, string text) { - if (ctx.Reducer is Event<Reducer>.Reducer reducer) + if (ctx.Event is Event<Reducer>.Reducer reducer) { var e = reducer.ReducerEvent; if (e.CallerIdentity == local_identity && e.Status is Status.Failed(var error)) @@ -182,7 +182,7 @@ void ProcessThread(DbConnection conn, CancellationToken ct) { conn.FrameTick(); - ProcessCommands(conn.RemoteReducers); + ProcessCommands(conn.Reducers); Thread.Sleep(100); } diff --git a/examples~/quickstart/client/module_bindings/Message.cs b/examples~/quickstart/client/module_bindings/Message.cs index c0e52cf0..c63d4bfa 100644 --- a/examples~/quickstart/client/module_bindings/Message.cs +++ b/examples~/quickstart/client/module_bindings/Message.cs @@ -7,7 +7,6 @@ using System; using SpacetimeDB; using System.Collections.Generic; -using System.Linq; using System.Runtime.Serialization; namespace SpacetimeDB.Types @@ -39,5 +38,6 @@ public Message() this.Sender = new(); this.Text = ""; } + } } diff --git a/examples~/quickstart/client/module_bindings/SendMessageReducer.cs b/examples~/quickstart/client/module_bindings/SendMessageReducer.cs index 45dd29f3..15106671 100644 --- a/examples~/quickstart/client/module_bindings/SendMessageReducer.cs +++ b/examples~/quickstart/client/module_bindings/SendMessageReducer.cs @@ -10,7 +10,7 @@ namespace SpacetimeDB.Types { [SpacetimeDB.Type] - public partial class SendMessageArgsStruct : IReducerArgs + public partial class SendMessage : IReducerArgs { string IReducerArgs.ReducerName => "send_message"; diff --git a/examples~/quickstart/client/module_bindings/SetNameReducer.cs b/examples~/quickstart/client/module_bindings/SetNameReducer.cs index 0f96ad9e..83a0b728 100644 --- a/examples~/quickstart/client/module_bindings/SetNameReducer.cs +++ b/examples~/quickstart/client/module_bindings/SetNameReducer.cs @@ -10,7 +10,7 @@ namespace SpacetimeDB.Types { [SpacetimeDB.Type] - public partial class SetNameArgsStruct : IReducerArgs + public partial class SetName : IReducerArgs { string IReducerArgs.ReducerName => "set_name"; diff --git a/examples~/quickstart/client/module_bindings/User.cs b/examples~/quickstart/client/module_bindings/User.cs index a6d6d1b9..2d4c79b0 100644 --- a/examples~/quickstart/client/module_bindings/User.cs +++ b/examples~/quickstart/client/module_bindings/User.cs @@ -7,7 +7,6 @@ using System; using SpacetimeDB; using System.Collections.Generic; -using System.Linq; using System.Runtime.Serialization; namespace SpacetimeDB.Types @@ -38,5 +37,6 @@ public User() { this.Identity = new(); } + } } diff --git a/examples~/quickstart/client/module_bindings/_Globals/SpacetimeDBClient.cs b/examples~/quickstart/client/module_bindings/_Globals/SpacetimeDBClient.cs index cf4f4e95..c0fb266b 100644 --- a/examples~/quickstart/client/module_bindings/_Globals/SpacetimeDBClient.cs +++ b/examples~/quickstart/client/module_bindings/_Globals/SpacetimeDBClient.cs @@ -5,75 +5,74 @@ #nullable enable using System; -using System.Collections.Generic; - +using SpacetimeDB; using SpacetimeDB.ClientApi; +using System.Collections.Generic; namespace SpacetimeDB.Types { public sealed class RemoteTables { - public class MessageHandle : RemoteTableHandle<EventContext, Message> { - public IEnumerable<Message> FilterBySender(SpacetimeDB.Identity value) { - return Query(x => x.Sender == value); - } - - public IEnumerable<Message> FilterBySent(ulong value) { - return Query(x => x.Sent == value); - } - - public IEnumerable<Message> FilterByText(string value) { - return Query(x => x.Text == value); - } - } - - public class UserHandle : RemoteTableHandle<EventContext, User> { - public override object? GetPrimaryKey(IDatabaseRow row) => ((User)row).Identity; - - private Dictionary<SpacetimeDB.Identity, User> Identity_Index = new(16); - - public override void InternalInvokeValueInserted(IDatabaseRow row) { - var value = (User)row; - Identity_Index[value.Identity] = value; - } - - public override void InternalInvokeValueDeleted(IDatabaseRow row) { - Identity_Index.Remove(((User)row).Identity); - } - - public User? FindByIdentity(SpacetimeDB.Identity value) { - Identity_Index.TryGetValue(value, out var r); - return r; - } - - public IEnumerable<User> FilterByIdentity(SpacetimeDB.Identity value) { - if (FindByIdentity(value) is { } found) { - yield return found; - } - } - - public IEnumerable<User> FilterByOnline(bool value) { - return Query(x => x.Online == value); - } - } - - public readonly MessageHandle Message = new(); + public class MessageHandle : RemoteTableHandle<EventContext, Message> + { + internal MessageHandle() + { + } + + } + + public readonly MessageHandle Message = new(); + + public class UserHandle : RemoteTableHandle<EventContext, User> + { + private static Dictionary<SpacetimeDB.Identity, User> Identity_Index = new(16); + + public override void InternalInvokeValueInserted(IDatabaseRow row) + { + var value = (User)row; + Identity_Index[value.Identity] = value; + } + + public override void InternalInvokeValueDeleted(IDatabaseRow row) + { + Identity_Index.Remove(((User)row).Identity); + } + + public readonly ref struct IdentityUniqueIndex + { + public User? Find(SpacetimeDB.Identity value) + { + Identity_Index.TryGetValue(value, out var r); + return r; + } + + } + + public IdentityUniqueIndex Identity => new(); + + internal UserHandle() + { + } + public override object GetPrimaryKey(IDatabaseRow row) => ((User)row).Identity; + + } + public readonly UserHandle User = new(); + } public sealed class RemoteReducers : RemoteBase<DbConnection> { internal RemoteReducers(DbConnection conn) : base(conn) {} - public delegate void SendMessageHandler(EventContext ctx, string text); public event SendMessageHandler? OnSendMessage; public void SendMessage(string text) { - conn.InternalCallReducer(new SendMessageArgsStruct { Text = text }); + conn.InternalCallReducer(new SendMessage { Text = text }); } - public bool InvokeSendMessage(EventContext ctx, SendMessageArgsStruct args) + public bool InvokeSendMessage(EventContext ctx, SendMessage args) { if (OnSendMessage == null) return false; OnSendMessage( @@ -87,10 +86,10 @@ public bool InvokeSendMessage(EventContext ctx, SendMessageArgsStruct args) public void SetName(string name) { - conn.InternalCallReducer(new SetNameArgsStruct { Name = name }); + conn.InternalCallReducer(new SetName { Name = name }); } - public bool InvokeSetName(EventContext ctx, SetNameArgsStruct args) + public bool InvokeSetName(EventContext ctx, SetName args) { if (OnSetName == null) return false; OnSetName( @@ -101,64 +100,69 @@ public bool InvokeSetName(EventContext ctx, SetNameArgsStruct args) } } - public partial record EventContext : DbContext<RemoteTables>, IEventContext { + public partial record EventContext : DbContext<RemoteTables>, IEventContext + { public readonly RemoteReducers Reducers; - public readonly Event<Reducer> Reducer; + public readonly Event<Reducer> Event; - internal EventContext(DbConnection conn, Event<Reducer> reducer) : base(conn.RemoteTables) { - Reducers = conn.RemoteReducers; - Reducer = reducer; + internal EventContext(DbConnection conn, Event<Reducer> reducerEvent) : base(conn.Db) + { + Reducers = conn.Reducers; + Event = reducerEvent; } } [Type] public partial record Reducer : TaggedEnum<( - SendMessageArgsStruct SendMessage, - SetNameArgsStruct SetName, - Unit IdentityConnected, - Unit IdentityDisconnected - )>; - + SendMessage SendMessage, + SetName SetName, + Unit StdbNone, + Unit StdbIdentityConnected, + Unit StdbIdentityDisconnected + )>; public class DbConnection : DbConnectionBase<DbConnection, Reducer> { - public readonly RemoteTables RemoteTables = new(); - public readonly RemoteReducers RemoteReducers; + public readonly RemoteTables Db = new(); + public readonly RemoteReducers Reducers; public DbConnection() { - RemoteReducers = new(this); + Reducers = new(this); - clientDB.AddTable<Message>("Message", RemoteTables.Message); - clientDB.AddTable<User>("User", RemoteTables.User); + clientDB.AddTable<Message>("message", Db.Message); + clientDB.AddTable<User>("user", Db.User); } protected override Reducer ToReducer(TransactionUpdate update) { var encodedArgs = update.ReducerCall.Args; return update.ReducerCall.ReducerName switch { - "send_message" => new Reducer.SendMessage(BSATNHelpers.Decode<SendMessageArgsStruct>(encodedArgs)), - "set_name" => new Reducer.SetName(BSATNHelpers.Decode<SetNameArgsStruct>(encodedArgs)), - "__identity_connected__" => new Reducer.IdentityConnected(default), - "__identity_disconnected__" => new Reducer.IdentityDisconnected(default), + "send_message" => new Reducer.SendMessage(BSATNHelpers.Decode<SendMessage>(encodedArgs)), + "set_name" => new Reducer.SetName(BSATNHelpers.Decode<SetName>(encodedArgs)), + "<none>" => new Reducer.StdbNone(default), + "__identity_connected__" => new Reducer.StdbIdentityConnected(default), + "__identity_disconnected__" => new Reducer.StdbIdentityDisconnected(default), + "" => new Reducer.StdbNone(default), var reducer => throw new ArgumentOutOfRangeException("Reducer", $"Unknown reducer {reducer}") }; } - protected override IEventContext ToEventContext(Event<Reducer> reducerEvent) { - return new EventContext(this, reducerEvent); - } + protected override IEventContext ToEventContext(Event<Reducer> reducerEvent) => + new EventContext(this, reducerEvent); - protected override bool Dispatch(IEventContext context, Reducer reducer) { + protected override bool Dispatch(IEventContext context, Reducer reducer) + { var eventContext = (EventContext)context; return reducer switch { - Reducer.SendMessage(var args) => RemoteReducers.InvokeSendMessage(eventContext, args), - Reducer.SetName(var args) => RemoteReducers.InvokeSetName(eventContext, args), - Reducer.IdentityConnected => true, - Reducer.IdentityDisconnected => true, + Reducer.SendMessage(var args) => Reducers.InvokeSendMessage(eventContext, args), + Reducer.SetName(var args) => Reducers.InvokeSetName(eventContext, args), + Reducer.StdbNone or + Reducer.StdbIdentityConnected or + Reducer.StdbIdentityDisconnected => true, _ => throw new ArgumentOutOfRangeException("Reducer", $"Unknown reducer {reducer}") }; } - public SubscriptionBuilder<EventContext> SubscriptionBuilder() => new(this); + public SubscriptionBuilder<EventContext> SubscriptionBuilder() => new(this); } } diff --git a/examples~/quickstart/server/src/lib.rs b/examples~/quickstart/server/src/lib.rs index 0c6572e1..015aaefe 100644 --- a/examples~/quickstart/server/src/lib.rs +++ b/examples~/quickstart/server/src/lib.rs @@ -1,7 +1,7 @@ -use spacetimedb::{ReducerContext, Identity, Timestamp}; +use spacetimedb::{ReducerContext, Identity, Table, Timestamp}; use anyhow::{Result, anyhow}; -#[spacetimedb::table(name = User, public)] +#[spacetimedb::table(name = user, public)] pub struct User { #[primary_key] identity: Identity, @@ -9,7 +9,7 @@ pub struct User { online: bool, } -#[spacetimedb::table(name = Message, public)] +#[spacetimedb::table(name = message, public)] pub struct Message { sender: Identity, sent: Timestamp, @@ -17,20 +17,20 @@ pub struct Message { } #[spacetimedb::reducer(init)] -pub fn init() { - // Called when the module is initially published +pub fn init(_ctx: &ReducerContext) { + } #[spacetimedb::reducer(client_connected)] -pub fn identity_connected(ctx: ReducerContext) { - if let Some(user) = User::filter_by_identity(&ctx.sender) { +pub fn identity_connected(ctx: &ReducerContext) { + if let Some(user) = ctx.db.user().identity().find(&ctx.sender) { // If this is a returning user, i.e. we already have a `User` with this `Identity`, // set `online: true`, but leave `name` and `identity` unchanged. - User::update_by_identity(&ctx.sender, User { online: true, ..user }); + ctx.db.user().identity().update(User { online: true, ..user }); } else { // If this is a new user, create a `User` row for the `Identity`, // which is online, but hasn't set a name. - User::insert(User { + ctx.db.user().try_insert(User { name: None, identity: ctx.sender, online: true, @@ -39,9 +39,9 @@ pub fn identity_connected(ctx: ReducerContext) { } #[spacetimedb::reducer(client_disconnected)] -pub fn identity_disconnected(ctx: ReducerContext) { - if let Some(user) = User::filter_by_identity(&ctx.sender) { - User::update_by_identity(&ctx.sender, User { online: false, ..user }); +pub fn identity_disconnected(ctx: &ReducerContext) { + if let Some(user) = ctx.db.user().identity().find(&ctx.sender) { + ctx.db.user().identity().update(User { online: false, ..user }); } else { // This branch should be unreachable, // as it doesn't make sense for a client to disconnect without connecting first. @@ -58,10 +58,10 @@ fn validate_name(name: String) -> Result<String> { } #[spacetimedb::reducer] -pub fn set_name(ctx: ReducerContext, name: String) -> Result<()> { +pub fn set_name(ctx: &ReducerContext, name: String) -> Result<()> { let name = validate_name(name)?; - if let Some(user) = User::filter_by_identity(&ctx.sender) { - User::update_by_identity(&ctx.sender, User { name: Some(name), ..user }); + if let Some(user) = ctx.db.user().identity().find(&ctx.sender) { + ctx.db.user().identity().update(User { name: Some(name), ..user }); Ok(()) } else { Err(anyhow!("Cannot set name for unknown user")) @@ -77,12 +77,12 @@ fn validate_message(text: String) -> Result<String> { } #[spacetimedb::reducer] -pub fn send_message(ctx: ReducerContext, text: String) -> Result<()> { +pub fn send_message(ctx: &ReducerContext, text: String) -> Result<()> { // Things to consider: // - Rate-limit messages per-user. // - Reject messages from unnamed users. let text = validate_message(text)?; - Message::insert(Message { + ctx.db.message().insert(Message { sender: ctx.sender, text, sent: ctx.timestamp, diff --git a/src/SpacetimeDB/ClientApi/DatabaseUpdate.cs b/src/SpacetimeDB/ClientApi/DatabaseUpdate.cs index 3294c6b7..98460753 100644 --- a/src/SpacetimeDB/ClientApi/DatabaseUpdate.cs +++ b/src/SpacetimeDB/ClientApi/DatabaseUpdate.cs @@ -7,7 +7,6 @@ using System; using SpacetimeDB; using System.Collections.Generic; -using System.Linq; using System.Runtime.Serialization; namespace SpacetimeDB.ClientApi diff --git a/src/SpacetimeDB/ClientApi/EnergyQuanta.cs b/src/SpacetimeDB/ClientApi/EnergyQuanta.cs index 476712e9..e571a9e1 100644 --- a/src/SpacetimeDB/ClientApi/EnergyQuanta.cs +++ b/src/SpacetimeDB/ClientApi/EnergyQuanta.cs @@ -7,7 +7,6 @@ using System; using SpacetimeDB; using System.Collections.Generic; -using System.Linq; using System.Runtime.Serialization; namespace SpacetimeDB.ClientApi diff --git a/src/SpacetimeDB/ClientApi/IdentityToken.cs b/src/SpacetimeDB/ClientApi/IdentityToken.cs index 4a53ad2f..751fcd75 100644 --- a/src/SpacetimeDB/ClientApi/IdentityToken.cs +++ b/src/SpacetimeDB/ClientApi/IdentityToken.cs @@ -7,7 +7,6 @@ using System; using SpacetimeDB; using System.Collections.Generic; -using System.Linq; using System.Runtime.Serialization; namespace SpacetimeDB.ClientApi diff --git a/src/SpacetimeDB/ClientApi/InitialSubscription.cs b/src/SpacetimeDB/ClientApi/InitialSubscription.cs index cdbc73de..c6144101 100644 --- a/src/SpacetimeDB/ClientApi/InitialSubscription.cs +++ b/src/SpacetimeDB/ClientApi/InitialSubscription.cs @@ -7,7 +7,6 @@ using System; using SpacetimeDB; using System.Collections.Generic; -using System.Linq; using System.Runtime.Serialization; namespace SpacetimeDB.ClientApi diff --git a/src/SpacetimeDB/ClientApi/OneOffTable.cs b/src/SpacetimeDB/ClientApi/OneOffTable.cs index d1f48f3f..fe667b18 100644 --- a/src/SpacetimeDB/ClientApi/OneOffTable.cs +++ b/src/SpacetimeDB/ClientApi/OneOffTable.cs @@ -7,7 +7,6 @@ using System; using SpacetimeDB; using System.Collections.Generic; -using System.Linq; using System.Runtime.Serialization; namespace SpacetimeDB.ClientApi diff --git a/src/SpacetimeDB/ClientApi/Subscribe.cs b/src/SpacetimeDB/ClientApi/Subscribe.cs index 0adad9b2..c58f1a46 100644 --- a/src/SpacetimeDB/ClientApi/Subscribe.cs +++ b/src/SpacetimeDB/ClientApi/Subscribe.cs @@ -7,7 +7,6 @@ using System; using SpacetimeDB; using System.Collections.Generic; -using System.Linq; using System.Runtime.Serialization; namespace SpacetimeDB.ClientApi diff --git a/src/SpacetimeDB/ClientApi/TableUpdate.cs b/src/SpacetimeDB/ClientApi/TableUpdate.cs index cc2586d3..d3265065 100644 --- a/src/SpacetimeDB/ClientApi/TableUpdate.cs +++ b/src/SpacetimeDB/ClientApi/TableUpdate.cs @@ -7,7 +7,6 @@ using System; using SpacetimeDB; using System.Collections.Generic; -using System.Linq; using System.Runtime.Serialization; namespace SpacetimeDB.ClientApi diff --git a/src/SpacetimeDB/ClientApi/Timestamp.cs b/src/SpacetimeDB/ClientApi/Timestamp.cs index c0f53ba1..92bd242d 100644 --- a/src/SpacetimeDB/ClientApi/Timestamp.cs +++ b/src/SpacetimeDB/ClientApi/Timestamp.cs @@ -7,7 +7,6 @@ using System; using SpacetimeDB; using System.Collections.Generic; -using System.Linq; using System.Runtime.Serialization; namespace SpacetimeDB.ClientApi diff --git a/src/SpacetimeDB/ClientApi/TransactionUpdate.cs b/src/SpacetimeDB/ClientApi/TransactionUpdate.cs index e4925bf4..d2c349ce 100644 --- a/src/SpacetimeDB/ClientApi/TransactionUpdate.cs +++ b/src/SpacetimeDB/ClientApi/TransactionUpdate.cs @@ -7,7 +7,6 @@ using System; using SpacetimeDB; using System.Collections.Generic; -using System.Linq; using System.Runtime.Serialization; namespace SpacetimeDB.ClientApi diff --git a/tests~/SnapshotTests.VerifyAllTablesParsed.verified.txt b/tests~/SnapshotTests.VerifyAllTablesParsed.verified.txt index 53528975..ea57bb0e 100644 --- a/tests~/SnapshotTests.VerifyAllTablesParsed.verified.txt +++ b/tests~/SnapshotTests.VerifyAllTablesParsed.verified.txt @@ -8,7 +8,7 @@ OnInsertUser: { eventContext: { Reducers: {Scrubbed}, - Reducer: {}, + Event: {}, Db: {Scrubbed} }, user: { @@ -20,7 +20,7 @@ OnInsertUser: { eventContext: { Reducers: {Scrubbed}, - Reducer: { + Event: { ReducerEvent: { Timestamp: DateTimeOffset_1, Status: {}, @@ -40,7 +40,7 @@ OnUpdateUser: { eventContext: { Reducers: {Scrubbed}, - Reducer: { + Event: { ReducerEvent: { Timestamp: DateTimeOffset_2, Status: {}, @@ -68,7 +68,7 @@ }, OnSetName: { Reducers: {Scrubbed}, - Reducer: { + Event: { ReducerEvent: { Timestamp: DateTimeOffset_2, Status: {}, @@ -87,7 +87,7 @@ OnInsertMessage: { eventContext: { Reducers: {Scrubbed}, - Reducer: { + Event: { ReducerEvent: { Timestamp: DateTimeOffset_3, Status: {}, @@ -111,7 +111,7 @@ }, OnSendMessage: { Reducers: {Scrubbed}, - Reducer: { + Event: { ReducerEvent: { Timestamp: DateTimeOffset_3, Status: {}, @@ -130,7 +130,7 @@ OnUpdateUser: { eventContext: { Reducers: {Scrubbed}, - Reducer: { + Event: { ReducerEvent: { Timestamp: DateTimeOffset_4, Status: {}, @@ -158,7 +158,7 @@ }, OnSetName: { Reducers: {Scrubbed}, - Reducer: { + Event: { ReducerEvent: { Timestamp: DateTimeOffset_4, Status: {}, @@ -177,7 +177,7 @@ OnInsertMessage: { eventContext: { Reducers: {Scrubbed}, - Reducer: { + Event: { ReducerEvent: { Timestamp: DateTimeOffset_5, Status: {}, @@ -201,7 +201,7 @@ }, OnSendMessage: { Reducers: {Scrubbed}, - Reducer: { + Event: { ReducerEvent: { Timestamp: DateTimeOffset_5, Status: {}, @@ -220,7 +220,7 @@ OnInsertMessage: { eventContext: { Reducers: {Scrubbed}, - Reducer: { + Event: { ReducerEvent: { Timestamp: DateTimeOffset_6, Status: {}, @@ -244,7 +244,7 @@ }, OnSendMessage: { Reducers: {Scrubbed}, - Reducer: { + Event: { ReducerEvent: { Timestamp: DateTimeOffset_6, Status: {}, @@ -263,7 +263,7 @@ OnUpdateUser: { eventContext: { Reducers: {Scrubbed}, - Reducer: { + Event: { ReducerEvent: { Timestamp: DateTimeOffset_7, Status: {}, @@ -289,7 +289,7 @@ OnInsertMessage: { eventContext: { Reducers: {Scrubbed}, - Reducer: { + Event: { ReducerEvent: { Timestamp: DateTimeOffset_8, Status: {}, @@ -313,7 +313,7 @@ }, OnSendMessage: { Reducers: {Scrubbed}, - Reducer: { + Event: { ReducerEvent: { Timestamp: DateTimeOffset_8, Status: {}, @@ -390,4 +390,4 @@ Max: type=InitialSubscription } } -} +} \ No newline at end of file diff --git a/tests~/SnapshotTests.cs b/tests~/SnapshotTests.cs index 8e99242e..d293c126 100644 --- a/tests~/SnapshotTests.cs +++ b/tests~/SnapshotTests.cs @@ -190,7 +190,7 @@ private static byte[] Encode<T>(in T value) where T : IStructuralReadWrite } private static TableUpdate SampleUserInsert(string identity, string? name, bool online) => - SampleUpdate(4097, "User", [new User + SampleUpdate(4097, "user", [new User { Identity = Identity.From(Convert.FromBase64String(identity)), Name = name, @@ -198,7 +198,7 @@ private static TableUpdate SampleUserInsert(string identity, string? name, bool }], []); private static TableUpdate SampleUserUpdate(string identity, string? oldName, string? newName, bool oldOnline, bool newOnline) => - SampleUpdate(4097, "User", [new User + SampleUpdate(4097, "user", [new User { Identity = Identity.From(Convert.FromBase64String(identity)), Name = newName, @@ -211,7 +211,7 @@ private static TableUpdate SampleUserUpdate(string identity, string? oldName, st }]); private static TableUpdate SampleMessage(string identity, ulong sent, string text) => - SampleUpdate(4098, "Message", [new Message + SampleUpdate(4098, "message", [new Message { Sender = Identity.From(Convert.FromBase64String(identity)), Sent = sent, @@ -238,27 +238,27 @@ private static ServerMessage[] SampleDump() => [ SampleTransactionUpdate( 1718487768057579, "j5DMlKmWjfbSl7qmZQOok7HDSwsAJopRSJjdlUsNogs=", "Vd4dFzcEzhLHJ6uNL8VXFg==", 1, "set_name", 4345615, 70, [SampleUserUpdate("j5DMlKmWjfbSl7qmZQOok7HDSwsAJopRSJjdlUsNogs=", null, "A", true, true)], - Encode(new SetNameArgsStruct { Name = "A" }) + Encode(new SetName { Name = "A" }) ), SampleTransactionUpdate( 1718487775346381, "l0qzG1GPRtC1mwr+54q98tv0325gozLc6cNzq4vrzqY=", "Kwmeu5riP20rvCTNbBipLA==", 1, "send_message", 2779615, 57, [SampleMessage("l0qzG1GPRtC1mwr+54q98tv0325gozLc6cNzq4vrzqY=", 1718487775346381, "Hello, A!")], - Encode(new SendMessageArgsStruct { Text = "Hello, A!" }) + Encode(new SendMessage { Text = "Hello, A!" }) ), SampleTransactionUpdate( 1718487777307855, "l0qzG1GPRtC1mwr+54q98tv0325gozLc6cNzq4vrzqY=", "Kwmeu5riP20rvCTNbBipLA==", 2, "set_name", 4268615, 98, [SampleUserUpdate("l0qzG1GPRtC1mwr+54q98tv0325gozLc6cNzq4vrzqY=", null, "B", true, true)], - Encode(new SetNameArgsStruct { Name = "B" }) + Encode(new SetName { Name = "B" }) ), SampleTransactionUpdate( 1718487783175083, "j5DMlKmWjfbSl7qmZQOok7HDSwsAJopRSJjdlUsNogs=", "Vd4dFzcEzhLHJ6uNL8VXFg==", 2, "send_message", 2677615, 40, [SampleMessage("j5DMlKmWjfbSl7qmZQOok7HDSwsAJopRSJjdlUsNogs=", 1718487783175083, "Hello, B!")], - Encode(new SendMessageArgsStruct { Text = "Hello, B!" }) + Encode(new SendMessage { Text = "Hello, B!" }) ), SampleTransactionUpdate( 1718487787645364, "l0qzG1GPRtC1mwr+54q98tv0325gozLc6cNzq4vrzqY=", "Kwmeu5riP20rvCTNbBipLA==", 3, "send_message", 2636615, 28, [SampleMessage("l0qzG1GPRtC1mwr+54q98tv0325gozLc6cNzq4vrzqY=", 1718487787645364, "Goodbye!")], - Encode(new SendMessageArgsStruct { Text = "Goodbye!" }) + Encode(new SendMessage { Text = "Goodbye!" }) ), SampleTransactionUpdate( 1718487791901504, "l0qzG1GPRtC1mwr+54q98tv0325gozLc6cNzq4vrzqY=", "Kwmeu5riP20rvCTNbBipLA==", @@ -268,7 +268,7 @@ private static ServerMessage[] SampleDump() => [ SampleTransactionUpdate( 1718487794937841, "j5DMlKmWjfbSl7qmZQOok7HDSwsAJopRSJjdlUsNogs=", "Vd4dFzcEzhLHJ6uNL8VXFg==", 3, "send_message", 2636615, 34, [SampleMessage("j5DMlKmWjfbSl7qmZQOok7HDSwsAJopRSJjdlUsNogs=", 1718487794937841, "Goodbye!")], - Encode(new SendMessageArgsStruct { Text = "Goodbye!" }) + Encode(new SendMessage { Text = "Goodbye!" }) ), ]; @@ -319,15 +319,15 @@ public async Task VerifyAllTablesParsed() client.onUnhandledReducerError += (exception) => events.Add("OnUnhandledReducerError", exception); #pragma warning restore CS0612 // Using obsolete API - client.RemoteReducers.OnSendMessage += (eventContext, _text) => + client.Reducers.OnSendMessage += (eventContext, _text) => events.Add("OnSendMessage", eventContext); - client.RemoteReducers.OnSetName += (eventContext, _name) => events.Add("OnSetName", eventContext); + client.Reducers.OnSetName += (eventContext, _name) => events.Add("OnSetName", eventContext); - client.RemoteTables.User.OnDelete += (eventContext, user) => + client.Db.User.OnDelete += (eventContext, user) => events.Add("OnDeleteUser", new { eventContext, user }); - client.RemoteTables.User.OnInsert += (eventContext, user) => + client.Db.User.OnInsert += (eventContext, user) => events.Add("OnInsertUser", new { eventContext, user }); - client.RemoteTables.User.OnUpdate += (eventContext, oldUser, newUser) => + client.Db.User.OnUpdate += (eventContext, oldUser, newUser) => events.Add( "OnUpdateUser", new @@ -338,9 +338,9 @@ public async Task VerifyAllTablesParsed() } ); - client.RemoteTables.Message.OnDelete += (eventContext, message) => + client.Db.Message.OnDelete += (eventContext, message) => events.Add("OnDeleteMessage", new { eventContext, message }); - client.RemoteTables.Message.OnInsert += (eventContext, message) => + client.Db.Message.OnInsert += (eventContext, message) => events.Add("OnInsertMessage", new { eventContext, message }); // Simulate receiving WebSocket messages. @@ -362,8 +362,8 @@ await Verify( Events = events, FinalSnapshot = new { - User = client.RemoteTables.User.Iter().ToList(), - Message = client.RemoteTables.Message.Iter().ToList() + User = client.Db.User.Iter().ToList(), + Message = client.Db.Message.Iter().ToList() }, Stats = client.stats } From 2783385a435c8fae4d6cd02f1ecba3e7279449f3 Mon Sep 17 00:00:00 2001 From: Zeke Foppa <196249+bfops@users.noreply.github.com> Date: Thu, 3 Oct 2024 21:43:39 -0700 Subject: [PATCH 28/55] 0.12.0 DLLs (again) (#161) ## Description of Changes Update with the latest DLLs from the current 0.12.0 branch. ## API - [ ] This is an API breaking change to the SDK *If the API is breaking, please state below what will break* ## Requires SpacetimeDB PRs *List any PRs here that are required for this SDK change to work* --------- Co-authored-by: Zeke Foppa <bfops@users.noreply.github.com> --- .../dotnet/cs/SpacetimeDB.BSATN.Codegen.dll | Bin 57856 -> 57856 bytes .../SpacetimeDB.BSATN.Runtime.dll | Bin 68096 -> 68096 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/packages/spacetimedb.bsatn.runtime/0.12.0/analyzers/dotnet/cs/SpacetimeDB.BSATN.Codegen.dll b/packages/spacetimedb.bsatn.runtime/0.12.0/analyzers/dotnet/cs/SpacetimeDB.BSATN.Codegen.dll index 222b592781a64e8c004ffecde831368d31b1689e..a17cd51fa7ba5288355b2a1cbfa19eab65a012bf 100644 GIT binary patch delta 238 zcmZoT!rX9#c|r$^yk6ArjXisI2;_z8-cKk^_nB>z7uuimqIz@0&JszDWP>F0M3a<M zqhvEnBV%(@lOzlCw3HN60|OHa3$qmCG*bgJQzHwD&0jA4U}lN%lG`@<!?gf`3#QQx z_jUGqZDulx=Q<=+JUQU{QU$1>)n}+6NcGizDP8sBzgsu*7}jqVy%EahpUhyuki=ll zkjP-dkiwA4V8oEjV8&nxq>X{1ra;jo1`8l74Jexe<Qo8?2~flWs44}hCJiiR1|*F@ LqMLW#b7uwsMR7_9 delta 238 zcmZoT!rX9#c|r$^#jLU$8+-Qb5Lh*@Pm=$hgXg+?U+x~g(CxlCVrPk@hLJ&PlCep$ zp@C&`TB=2&g{6f_s%3IgYMP;;MWV5hrGbH|aiU48;pQ)welW8*sIBgt{NY-FK<w!a zwsxHT{;TUJ9#UX`bYybC^`#0>LH=y0AV{@TU)6+(&eLCf%Q-%Mv*?XbHh&`q1BO(F zBnD#!6NY34Lm<x*NT&hCEEp1j*b<0MfIN_B5|B;<sxbu188a9GMGb(^6sS89tjcin I&U^060J#oO6951J diff --git a/packages/spacetimedb.bsatn.runtime/0.12.0/lib/netstandard2.1/SpacetimeDB.BSATN.Runtime.dll b/packages/spacetimedb.bsatn.runtime/0.12.0/lib/netstandard2.1/SpacetimeDB.BSATN.Runtime.dll index 2f56513350acc70730d652af65dce9fa658f1928..4197c459741c228f3cd90f0f7ec497c9f29e1e59 100644 GIT binary patch delta 240 zcmZpe!qPB>WkLr_TB_}pjXgb21*VIaFn#JW^_)<pWO<C?ht6h=XSYQ)k`0o~6HQW5 zjgrkQjf~ArO_D6k(^67Q4Gc^yEX-1j(@YJ_OpPooHf#TR$jl;<6tH8e0Aql_@=Lxa zmF|D^Sai{&{=D?5im3&ROBJAk7D`Y-uxbfrZzJxO$9zWD?{7C{Wc<tGpUhyuki=ll zkjP-dkiwA4V8oEjV8&nxq>X{1ra;jo1`8l74Jexe<Qo8?2~flWs44}hCJiiR1|*F@ LqT3tU80(n<N@z(2 delta 240 zcmZpe!qPB>WkLswq?Jkk#-5(10&KkP6G}b>xlYyIqOj^;r_pAOXSYQ)j0{qfj7^da z4J?z>QY{iKEG<k@Et8W{(+mwQ5{->44Gc_;6HQVLH*5cS$jmbDM%Kou0*nCyrx$P0 zJHoZrbLqB8lebUhUOKgaaj61SkP{{dRBgB}?#rcoHkPL<fA@B6H)Uk}%i?duV8D>d zki=ljV8W2hU<l+{0_ilMm<2;35L*JV36KX8O#;$sKsAP7Ib#MRpr`>5ngVqvf>jxA KZ)9VvX9fVXn@pMj From c74b1fd6d26f1b36aa47ad994d6fe9c68d140b6d Mon Sep 17 00:00:00 2001 From: Jeremie Pelletier <jeremiep@gmail.com> Date: Sat, 12 Oct 2024 10:40:26 -0400 Subject: [PATCH 29/55] Jeremie/one off query decoupled from table (#163) ## Description of Changes *Describe what has been changed, any new features or bug fixes* ## API - [ ] This is an API breaking change to the SDK *If the API is breaking, please state below what will break* ## Requires SpacetimeDB PRs *List any PRs here that are required for this SDK change to work* --------- Co-authored-by: John Detter <no-reply@boppygames.gg> --- src/ClientCache.cs | 6 ++++++ src/SpacetimeDBClient.cs | 25 +++++++++++++++---------- src/Table.cs | 15 +++++++++++++++ 3 files changed, 36 insertions(+), 10 deletions(-) diff --git a/src/ClientCache.cs b/src/ClientCache.cs index 0ec8ad91..9e01e327 100644 --- a/src/ClientCache.cs +++ b/src/ClientCache.cs @@ -10,8 +10,12 @@ namespace SpacetimeDB // It should just provide auto-generated `GetTable` and `GetTables` methods. public class ClientCache { + private readonly IDbConnection conn; + private readonly Dictionary<string, IRemoteTableHandle> tables = new(); + public ClientCache(IDbConnection conn) => this.conn = conn; + public void AddTable<Row>(string name, IRemoteTableHandle table) where Row : IDatabaseRow, new() { @@ -19,6 +23,8 @@ public void AddTable<Row>(string name, IRemoteTableHandle table) { Log.Error($"Table with name already exists: {name}"); } + + table.Initialize(name, conn); } internal IRemoteTableHandle? GetTable(string name) diff --git a/src/SpacetimeDBClient.cs b/src/SpacetimeDBClient.cs index 5ba858f9..dc627f2f 100644 --- a/src/SpacetimeDBClient.cs +++ b/src/SpacetimeDBClient.cs @@ -82,6 +82,8 @@ public interface IDbConnection internal void Subscribe(ISubscriptionHandle handle, string[] querySqls); void FrameTick(); void Disconnect(); + + internal Task<T[]> RemoteQuery<T>(string query) where T : IDatabaseRow, new(); } public abstract class DbConnectionBase<DbConnection, Reducer> : IDbConnection @@ -129,7 +131,7 @@ struct DbOp internal WebSocket webSocket; private bool connectionClosed; - protected readonly ClientCache clientDB = new(); + protected readonly ClientCache clientDB; protected abstract Reducer ToReducer(TransactionUpdate update); protected abstract IEventContext ToEventContext(Event<Reducer> reducerEvent); @@ -142,6 +144,8 @@ struct DbOp protected DbConnectionBase() { + clientDB = new(this); + var options = new WebSocket.ConnectOptions { //v1.bin.spacetimedb @@ -830,24 +834,25 @@ void IDbConnection.Subscribe(ISubscriptionHandle handle, string[] querySqls) )); } - /// Usage: SpacetimeDBClientBase.instance.OneOffQuery<Message>("WHERE sender = \"bob\""); - public async Task<T[]> OneOffQuery<T>(string query) - where T : IDatabaseRow, new() + /// Usage: SpacetimeDBClientBase.instance.OneOffQuery<Message>("SELECT * FROM table WHERE sender = \"bob\""); + [Obsolete("This is replaced by ctx.Db.TableName.OneOffQuery(\"WHERE ...\")", false)] + public Task<T[]> OneOffQuery<T>(string query) where T : IDatabaseRow, new() => + ((IDbConnection)this).RemoteQuery<T>(query); + + async Task<T[]> IDbConnection.RemoteQuery<T>(string query) { var messageId = Guid.NewGuid(); - var type = typeof(T); var resultSource = new TaskCompletionSource<OneOffQueryResponse>(); waitingOneOffQueries[messageId] = resultSource; // unsanitized here, but writes will be prevented serverside. // the best they can do is send multiple selects, which will just result in them getting no data back. - string queryString = $"SELECT * FROM {type.Name} {query}"; var requestId = stats.OneOffRequestTracker.StartTrackingRequest(); webSocket.Send(new ClientMessage.OneOffQuery(new OneOffQuery { MessageId = messageId.ToByteArray(), - QueryString = queryString, + QueryString = query, })); // Suspend for an arbitrary amount of time @@ -860,7 +865,7 @@ public async Task<T[]> OneOffQuery<T>(string query) T[] LogAndThrow(string error) { - error = $"While processing one-off-query `{queryString}`, ID {messageId}: {error}"; + error = $"While processing one-off-query `{query}`, ID {messageId}: {error}"; Log.Error(error); throw new Exception(error); } @@ -879,9 +884,9 @@ T[] LogAndThrow(string error) var resultTable = result.Tables[0]; var cacheTable = clientDB.GetTable(resultTable.TableName); - if (cacheTable?.ClientTableType != type) + if (cacheTable?.ClientTableType != typeof(T)) { - return LogAndThrow($"Mismatched result type, expected {type} but got {resultTable.TableName}"); + return LogAndThrow($"Mismatched result type, expected {typeof(T)} but got {resultTable.TableName}"); } return BsatnRowListIter(resultTable.Rows) diff --git a/src/Table.cs b/src/Table.cs index 2f5f3c6e..d3ebf918 100644 --- a/src/Table.cs +++ b/src/Table.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using SpacetimeDB.BSATN; @@ -36,12 +37,23 @@ public interface IRemoteTableHandle internal void InvokeDelete(IEventContext context, IDatabaseRow row); internal void InvokeBeforeDelete(IEventContext context, IDatabaseRow row); internal void InvokeUpdate(IEventContext context, IDatabaseRow oldRow, IDatabaseRow newRow); + + internal void Initialize(string name, IDbConnection conn); } public abstract class RemoteTableHandle<EventContext, Row> : IRemoteTableHandle where EventContext : class, IEventContext where Row : IDatabaseRow, new() { + string? name; + IDbConnection? conn; + + void IRemoteTableHandle.Initialize(string name, IDbConnection conn) + { + this.name = name; + this.conn = conn; + } + // These methods need to be overridden by autogen. public virtual object? GetPrimaryKey(IDatabaseRow row) => null; public virtual void InternalInvokeValueInserted(IDatabaseRow row) { } @@ -93,6 +105,9 @@ bool IRemoteTableHandle.DeleteEntry(byte[] rowBytes) protected IEnumerable<Row> Query(Func<Row, bool> filter) => Iter().Where(filter); + public Task<Row[]> RemoteQuery(string query) => + conn!.RemoteQuery<Row>($"SELECT * FROM {name!} {query}"); + void IRemoteTableHandle.InvokeInsert(IEventContext context, IDatabaseRow row) => OnInsert?.Invoke((EventContext)context, (Row)row); From f1b8fa8dd034ec50cd60bc95991430d55d07b02d Mon Sep 17 00:00:00 2001 From: Mazdak Farrokhzad <twingoow@gmail.com> Date: Tue, 15 Oct 2024 21:47:40 +0200 Subject: [PATCH 30/55] Add gzip + none compression algos and let SDK pick compression (#155) ## Description of Changes Companion to https://github.com/clockworklabs/SpacetimeDB/pull/1802. ## Requires SpacetimeDB PRs https://github.com/clockworklabs/SpacetimeDB/pull/1802 --- src/Compression.cs | 9 ++++ .../ClientApi/CompressableQueryUpdate.cs | 3 +- src/SpacetimeDBClient.cs | 48 ++++++++++++------- src/WebSocket.cs | 4 +- 4 files changed, 43 insertions(+), 21 deletions(-) create mode 100644 src/Compression.cs diff --git a/src/Compression.cs b/src/Compression.cs new file mode 100644 index 00000000..8f630f99 --- /dev/null +++ b/src/Compression.cs @@ -0,0 +1,9 @@ +namespace SpacetimeDB +{ + public enum Compression + { + None, + Brotli, + Gzip, + } +} diff --git a/src/SpacetimeDB/ClientApi/CompressableQueryUpdate.cs b/src/SpacetimeDB/ClientApi/CompressableQueryUpdate.cs index af397a58..1af6fd27 100644 --- a/src/SpacetimeDB/ClientApi/CompressableQueryUpdate.cs +++ b/src/SpacetimeDB/ClientApi/CompressableQueryUpdate.cs @@ -12,6 +12,7 @@ namespace SpacetimeDB.ClientApi [SpacetimeDB.Type] public partial record CompressableQueryUpdate : SpacetimeDB.TaggedEnum<( SpacetimeDB.ClientApi.QueryUpdate Uncompressed, - byte[] Brotli + byte[] Brotli, + byte[] Gzip )>; } diff --git a/src/SpacetimeDBClient.cs b/src/SpacetimeDBClient.cs index dc627f2f..e076fa49 100644 --- a/src/SpacetimeDBClient.cs +++ b/src/SpacetimeDBClient.cs @@ -22,6 +22,7 @@ public sealed class DbConnectionBuilder<DbConnection, Reducer> string? uri; string? nameOrAddress; string? token; + Compression? compression; public DbConnection Build() { @@ -33,7 +34,7 @@ public DbConnection Build() { throw new InvalidOperationException("Building DbConnection with a null nameOrAddress. Call WithModuleName() first."); } - conn.Connect(token, uri, nameOrAddress); + conn.Connect(token, uri, nameOrAddress, compression ?? Compression.Brotli); #if UNITY_5_3_OR_NEWER SpacetimeDBNetworkManager.ActiveConnections.Add(conn); #endif @@ -58,6 +59,12 @@ public DbConnectionBuilder<DbConnection, Reducer> WithCredentials(in (Identity i return this; } + public DbConnectionBuilder<DbConnection, Reducer> WithCompression(Compression compression) + { + this.compression = compression; + return this; + } + public DbConnectionBuilder<DbConnection, Reducer> OnConnect(Action<DbConnection, Identity, string> cb) { conn.onConnect += (identity, token) => cb.Invoke(conn, identity, token); @@ -209,6 +216,17 @@ enum CompressionAlgos : byte { None = 0, Brotli = 1, + Gzip = 2, + } + + private static BinaryReader BrotliReader(Stream stream) + { + return new BinaryReader(new BrotliStream(stream, CompressionMode.Decompress)); + } + + private static BinaryReader GzipReader(Stream stream) + { + return new BinaryReader(new GZipStream(stream, CompressionMode.Decompress)); } private static ServerMessage DecompressDecodeMessage(byte[] bytes) @@ -221,16 +239,11 @@ private static ServerMessage DecompressDecodeMessage(byte[] bytes) switch (compression) { case CompressionAlgos.None: - { - using var binaryReader = new BinaryReader(stream); - return new ServerMessage.BSATN().Read(binaryReader); - } + return new ServerMessage.BSATN().Read(new BinaryReader(stream)); case CompressionAlgos.Brotli: - { - using var decompressedStream = new BrotliStream(stream, CompressionMode.Decompress); - using var binaryReader = new BinaryReader(decompressedStream); - return new ServerMessage.BSATN().Read(binaryReader); - } + return new ServerMessage.BSATN().Read(BrotliReader(stream)); + case CompressionAlgos.Gzip: + return new ServerMessage.BSATN().Read(GzipReader(stream)); default: throw new InvalidOperationException("Unknown compression type"); } @@ -244,12 +257,11 @@ private static QueryUpdate DecompressDecodeQueryUpdate(CompressableQueryUpdate u return qu; case CompressableQueryUpdate.Brotli(var bytes): - { - using var stream = new MemoryStream(bytes); - using var decompressedStream = new BrotliStream(stream, CompressionMode.Decompress); - using var binaryReader = new BinaryReader(decompressedStream); - return new QueryUpdate.BSATN().Read(binaryReader); - } + return new QueryUpdate.BSATN().Read(BrotliReader(new MemoryStream(bytes))); + + case CompressableQueryUpdate.Gzip(var bytes): + return new QueryUpdate.BSATN().Read(GzipReader(new MemoryStream(bytes))); + default: throw new InvalidOperationException(); } @@ -579,7 +591,7 @@ public void Disconnect() /// </summary> /// <param name="uri"> URI of the SpacetimeDB server (ex: https://testnet.spacetimedb.com) /// <param name="addressOrName">The name or address of the database to connect to</param> - internal void Connect(string? token, string uri, string addressOrName) + internal void Connect(string? token, string uri, string addressOrName, Compression compression) { isClosing = false; @@ -597,7 +609,7 @@ internal void Connect(string? token, string uri, string addressOrName) { try { - await webSocket.Connect(token, uri, addressOrName, Address); + await webSocket.Connect(token, uri, addressOrName, Address, compression); } catch (Exception e) { diff --git a/src/WebSocket.cs b/src/WebSocket.cs index cd22c26f..dd0dae75 100644 --- a/src/WebSocket.cs +++ b/src/WebSocket.cs @@ -50,9 +50,9 @@ public WebSocket(ConnectOptions options) public bool IsConnected { get { return Ws != null && Ws.State == WebSocketState.Open; } } - public async Task Connect(string? auth, string host, string nameOrAddress, Address clientAddress) + public async Task Connect(string? auth, string host, string nameOrAddress, Address clientAddress, Compression compression) { - var url = new Uri($"{host}/database/subscribe/{nameOrAddress}?client_address={clientAddress}"); + var url = new Uri($"{host}/database/subscribe/{nameOrAddress}?client_address={clientAddress}&compression={nameof(compression)}"); Ws.Options.AddSubProtocol(_options.Protocol); var source = new CancellationTokenSource(10000); From 17e0c271d62ae330d6b276a64bceae6e37b203aa Mon Sep 17 00:00:00 2001 From: John Detter <4099508+jdetter@users.noreply.github.com> Date: Wed, 16 Oct 2024 00:05:17 -0500 Subject: [PATCH 31/55] Revert PR 155 (#173) ## Description of Changes *Describe what has been changed, any new features or bug fixes* PR 155 introduced a build issue in Unity: ![image](https://github.com/user-attachments/assets/7e88a813-93bd-4b74-ad87-a4c821a7fb98) This PR reverts back to a known working commit. I have tested after reverting the commit and the branch is back to working properly. ## API - [ ] This is an API breaking change to the SDK *If the API is breaking, please state below what will break* Not breaking ## Requires SpacetimeDB PRs *List any PRs here that are required for this SDK change to work* ## Testing *Write instructions for a test that you performed for this PR* - [x] Tested circle game against this commit and it builds + works Co-authored-by: John Detter <no-reply@boppygames.gg> --- src/Compression.cs | 9 ---- .../ClientApi/CompressableQueryUpdate.cs | 3 +- src/SpacetimeDBClient.cs | 48 +++++++------------ src/WebSocket.cs | 4 +- 4 files changed, 21 insertions(+), 43 deletions(-) delete mode 100644 src/Compression.cs diff --git a/src/Compression.cs b/src/Compression.cs deleted file mode 100644 index 8f630f99..00000000 --- a/src/Compression.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace SpacetimeDB -{ - public enum Compression - { - None, - Brotli, - Gzip, - } -} diff --git a/src/SpacetimeDB/ClientApi/CompressableQueryUpdate.cs b/src/SpacetimeDB/ClientApi/CompressableQueryUpdate.cs index 1af6fd27..af397a58 100644 --- a/src/SpacetimeDB/ClientApi/CompressableQueryUpdate.cs +++ b/src/SpacetimeDB/ClientApi/CompressableQueryUpdate.cs @@ -12,7 +12,6 @@ namespace SpacetimeDB.ClientApi [SpacetimeDB.Type] public partial record CompressableQueryUpdate : SpacetimeDB.TaggedEnum<( SpacetimeDB.ClientApi.QueryUpdate Uncompressed, - byte[] Brotli, - byte[] Gzip + byte[] Brotli )>; } diff --git a/src/SpacetimeDBClient.cs b/src/SpacetimeDBClient.cs index e076fa49..dc627f2f 100644 --- a/src/SpacetimeDBClient.cs +++ b/src/SpacetimeDBClient.cs @@ -22,7 +22,6 @@ public sealed class DbConnectionBuilder<DbConnection, Reducer> string? uri; string? nameOrAddress; string? token; - Compression? compression; public DbConnection Build() { @@ -34,7 +33,7 @@ public DbConnection Build() { throw new InvalidOperationException("Building DbConnection with a null nameOrAddress. Call WithModuleName() first."); } - conn.Connect(token, uri, nameOrAddress, compression ?? Compression.Brotli); + conn.Connect(token, uri, nameOrAddress); #if UNITY_5_3_OR_NEWER SpacetimeDBNetworkManager.ActiveConnections.Add(conn); #endif @@ -59,12 +58,6 @@ public DbConnectionBuilder<DbConnection, Reducer> WithCredentials(in (Identity i return this; } - public DbConnectionBuilder<DbConnection, Reducer> WithCompression(Compression compression) - { - this.compression = compression; - return this; - } - public DbConnectionBuilder<DbConnection, Reducer> OnConnect(Action<DbConnection, Identity, string> cb) { conn.onConnect += (identity, token) => cb.Invoke(conn, identity, token); @@ -216,17 +209,6 @@ enum CompressionAlgos : byte { None = 0, Brotli = 1, - Gzip = 2, - } - - private static BinaryReader BrotliReader(Stream stream) - { - return new BinaryReader(new BrotliStream(stream, CompressionMode.Decompress)); - } - - private static BinaryReader GzipReader(Stream stream) - { - return new BinaryReader(new GZipStream(stream, CompressionMode.Decompress)); } private static ServerMessage DecompressDecodeMessage(byte[] bytes) @@ -239,11 +221,16 @@ private static ServerMessage DecompressDecodeMessage(byte[] bytes) switch (compression) { case CompressionAlgos.None: - return new ServerMessage.BSATN().Read(new BinaryReader(stream)); + { + using var binaryReader = new BinaryReader(stream); + return new ServerMessage.BSATN().Read(binaryReader); + } case CompressionAlgos.Brotli: - return new ServerMessage.BSATN().Read(BrotliReader(stream)); - case CompressionAlgos.Gzip: - return new ServerMessage.BSATN().Read(GzipReader(stream)); + { + using var decompressedStream = new BrotliStream(stream, CompressionMode.Decompress); + using var binaryReader = new BinaryReader(decompressedStream); + return new ServerMessage.BSATN().Read(binaryReader); + } default: throw new InvalidOperationException("Unknown compression type"); } @@ -257,11 +244,12 @@ private static QueryUpdate DecompressDecodeQueryUpdate(CompressableQueryUpdate u return qu; case CompressableQueryUpdate.Brotli(var bytes): - return new QueryUpdate.BSATN().Read(BrotliReader(new MemoryStream(bytes))); - - case CompressableQueryUpdate.Gzip(var bytes): - return new QueryUpdate.BSATN().Read(GzipReader(new MemoryStream(bytes))); - + { + using var stream = new MemoryStream(bytes); + using var decompressedStream = new BrotliStream(stream, CompressionMode.Decompress); + using var binaryReader = new BinaryReader(decompressedStream); + return new QueryUpdate.BSATN().Read(binaryReader); + } default: throw new InvalidOperationException(); } @@ -591,7 +579,7 @@ public void Disconnect() /// </summary> /// <param name="uri"> URI of the SpacetimeDB server (ex: https://testnet.spacetimedb.com) /// <param name="addressOrName">The name or address of the database to connect to</param> - internal void Connect(string? token, string uri, string addressOrName, Compression compression) + internal void Connect(string? token, string uri, string addressOrName) { isClosing = false; @@ -609,7 +597,7 @@ internal void Connect(string? token, string uri, string addressOrName, Compressi { try { - await webSocket.Connect(token, uri, addressOrName, Address, compression); + await webSocket.Connect(token, uri, addressOrName, Address); } catch (Exception e) { diff --git a/src/WebSocket.cs b/src/WebSocket.cs index dd0dae75..cd22c26f 100644 --- a/src/WebSocket.cs +++ b/src/WebSocket.cs @@ -50,9 +50,9 @@ public WebSocket(ConnectOptions options) public bool IsConnected { get { return Ws != null && Ws.State == WebSocketState.Open; } } - public async Task Connect(string? auth, string host, string nameOrAddress, Address clientAddress, Compression compression) + public async Task Connect(string? auth, string host, string nameOrAddress, Address clientAddress) { - var url = new Uri($"{host}/database/subscribe/{nameOrAddress}?client_address={clientAddress}&compression={nameof(compression)}"); + var url = new Uri($"{host}/database/subscribe/{nameOrAddress}?client_address={clientAddress}"); Ws.Options.AddSubProtocol(_options.Protocol); var source = new CancellationTokenSource(10000); From d938d6d50fec20cb8d1dd563530d6df7a97f3c3d Mon Sep 17 00:00:00 2001 From: Zeke Foppa <196249+bfops@users.noreply.github.com> Date: Tue, 29 Oct 2024 11:33:15 -0700 Subject: [PATCH 32/55] Update DLLs and bump package versions to `1.0.0-rc1` (#180) ## Description of Changes Update the SpacetimeDB C# DLLs to be up to date with `master` in SpacetimeDB (now that C# bindings have been bumped to `v1.0.0-rc1`). We will need a followup PR ## API Not a breaking change. ## Requires SpacetimeDB PRs `master` ## Testing Only automated tests --------- Co-authored-by: Zeke Foppa <bfops@users.noreply.github.com> --- SpacetimeDB.ClientSDK.csproj | 6 +++--- package.json | 2 +- .../SpacetimeDB.BSATN.Runtime.dll | Bin 68096 -> 0 bytes .../{0.12.0.meta => 1.0.0.meta} | 0 .../{0.12.0 => 1.0.0}/analyzers.meta | 0 .../{0.12.0 => 1.0.0}/analyzers/dotnet.meta | 0 .../analyzers/dotnet/cs.meta | 0 .../dotnet/cs/SpacetimeDB.BSATN.Codegen.dll | Bin 57856 -> 57344 bytes .../cs/SpacetimeDB.BSATN.Codegen.dll.meta | 0 .../{0.12.0 => 1.0.0}/lib.meta | 0 .../{0.12.0 => 1.0.0}/lib/netstandard2.1.meta | 0 .../SpacetimeDB.BSATN.Runtime.dll | Bin 0 -> 64512 bytes .../SpacetimeDB.BSATN.Runtime.dll.meta | 0 13 files changed, 4 insertions(+), 4 deletions(-) delete mode 100644 packages/spacetimedb.bsatn.runtime/0.12.0/lib/netstandard2.1/SpacetimeDB.BSATN.Runtime.dll rename packages/spacetimedb.bsatn.runtime/{0.12.0.meta => 1.0.0.meta} (100%) rename packages/spacetimedb.bsatn.runtime/{0.12.0 => 1.0.0}/analyzers.meta (100%) rename packages/spacetimedb.bsatn.runtime/{0.12.0 => 1.0.0}/analyzers/dotnet.meta (100%) rename packages/spacetimedb.bsatn.runtime/{0.12.0 => 1.0.0}/analyzers/dotnet/cs.meta (100%) rename packages/spacetimedb.bsatn.runtime/{0.12.0 => 1.0.0}/analyzers/dotnet/cs/SpacetimeDB.BSATN.Codegen.dll (83%) rename packages/spacetimedb.bsatn.runtime/{0.12.0 => 1.0.0}/analyzers/dotnet/cs/SpacetimeDB.BSATN.Codegen.dll.meta (100%) rename packages/spacetimedb.bsatn.runtime/{0.12.0 => 1.0.0}/lib.meta (100%) rename packages/spacetimedb.bsatn.runtime/{0.12.0 => 1.0.0}/lib/netstandard2.1.meta (100%) create mode 100644 packages/spacetimedb.bsatn.runtime/1.0.0/lib/netstandard2.1/SpacetimeDB.BSATN.Runtime.dll rename packages/spacetimedb.bsatn.runtime/{0.12.0 => 1.0.0}/lib/netstandard2.1/SpacetimeDB.BSATN.Runtime.dll.meta (100%) diff --git a/SpacetimeDB.ClientSDK.csproj b/SpacetimeDB.ClientSDK.csproj index eb7773db..6bf4edcf 100644 --- a/SpacetimeDB.ClientSDK.csproj +++ b/SpacetimeDB.ClientSDK.csproj @@ -16,15 +16,15 @@ <PackageIcon>logo.png</PackageIcon> <PackageReadmeFile>README.md</PackageReadmeFile> <RepositoryUrl>https://github.com/clockworklabs/com.clockworklabs.spacetimedbsdk</RepositoryUrl> - <AssemblyVersion>0.12.0</AssemblyVersion> - <Version>$(AssemblyVersion)</Version> + <AssemblyVersion>1.0.0</AssemblyVersion> + <Version>1.0.0-rc1</Version> <DefaultItemExcludes>$(DefaultItemExcludes);*~/**</DefaultItemExcludes> <!-- We want to save DLLs for Unity which doesn't support NuGet. --> <RestorePackagesPath>packages</RestorePackagesPath> </PropertyGroup> <ItemGroup> - <PackageReference Include="SpacetimeDB.BSATN.Runtime" Version="0.12.*" /> + <PackageReference Include="SpacetimeDB.BSATN.Runtime" Version="1.0.0-rc1" /> <InternalsVisibleTo Include="SpacetimeDB.Tests" /> </ItemGroup> diff --git a/package.json b/package.json index 2bf0c6f8..015b4e0a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "com.clockworklabs.spacetimedbsdk", "displayName": "SpacetimeDB SDK", - "version": "0.12.0", + "version": "1.0.0", "description": "The SpacetimeDB Client SDK is a software development kit (SDK) designed to interact with and manipulate SpacetimeDB modules..", "keywords": [], "author": { diff --git a/packages/spacetimedb.bsatn.runtime/0.12.0/lib/netstandard2.1/SpacetimeDB.BSATN.Runtime.dll b/packages/spacetimedb.bsatn.runtime/0.12.0/lib/netstandard2.1/SpacetimeDB.BSATN.Runtime.dll deleted file mode 100644 index 4197c459741c228f3cd90f0f7ec497c9f29e1e59..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 68096 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 diff --git a/packages/spacetimedb.bsatn.runtime/0.12.0.meta b/packages/spacetimedb.bsatn.runtime/1.0.0.meta similarity index 100% rename from packages/spacetimedb.bsatn.runtime/0.12.0.meta rename to packages/spacetimedb.bsatn.runtime/1.0.0.meta diff --git a/packages/spacetimedb.bsatn.runtime/0.12.0/analyzers.meta b/packages/spacetimedb.bsatn.runtime/1.0.0/analyzers.meta similarity index 100% rename from packages/spacetimedb.bsatn.runtime/0.12.0/analyzers.meta rename to packages/spacetimedb.bsatn.runtime/1.0.0/analyzers.meta diff --git a/packages/spacetimedb.bsatn.runtime/0.12.0/analyzers/dotnet.meta b/packages/spacetimedb.bsatn.runtime/1.0.0/analyzers/dotnet.meta similarity index 100% rename from packages/spacetimedb.bsatn.runtime/0.12.0/analyzers/dotnet.meta rename to packages/spacetimedb.bsatn.runtime/1.0.0/analyzers/dotnet.meta diff --git a/packages/spacetimedb.bsatn.runtime/0.12.0/analyzers/dotnet/cs.meta b/packages/spacetimedb.bsatn.runtime/1.0.0/analyzers/dotnet/cs.meta similarity index 100% rename from packages/spacetimedb.bsatn.runtime/0.12.0/analyzers/dotnet/cs.meta rename to packages/spacetimedb.bsatn.runtime/1.0.0/analyzers/dotnet/cs.meta diff --git a/packages/spacetimedb.bsatn.runtime/0.12.0/analyzers/dotnet/cs/SpacetimeDB.BSATN.Codegen.dll b/packages/spacetimedb.bsatn.runtime/1.0.0/analyzers/dotnet/cs/SpacetimeDB.BSATN.Codegen.dll similarity index 83% rename from packages/spacetimedb.bsatn.runtime/0.12.0/analyzers/dotnet/cs/SpacetimeDB.BSATN.Codegen.dll rename to packages/spacetimedb.bsatn.runtime/1.0.0/analyzers/dotnet/cs/SpacetimeDB.BSATN.Codegen.dll index a17cd51fa7ba5288355b2a1cbfa19eab65a012bf..bcbcf0ef5a8048fa49abe3ed5f29c7e968896c71 100644 GIT binary patch delta 4417 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 Jb+$dz^ndfJ5mNvF delta 4533 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 diff --git a/packages/spacetimedb.bsatn.runtime/0.12.0/analyzers/dotnet/cs/SpacetimeDB.BSATN.Codegen.dll.meta b/packages/spacetimedb.bsatn.runtime/1.0.0/analyzers/dotnet/cs/SpacetimeDB.BSATN.Codegen.dll.meta similarity index 100% rename from packages/spacetimedb.bsatn.runtime/0.12.0/analyzers/dotnet/cs/SpacetimeDB.BSATN.Codegen.dll.meta rename to packages/spacetimedb.bsatn.runtime/1.0.0/analyzers/dotnet/cs/SpacetimeDB.BSATN.Codegen.dll.meta diff --git a/packages/spacetimedb.bsatn.runtime/0.12.0/lib.meta b/packages/spacetimedb.bsatn.runtime/1.0.0/lib.meta similarity index 100% rename from packages/spacetimedb.bsatn.runtime/0.12.0/lib.meta rename to packages/spacetimedb.bsatn.runtime/1.0.0/lib.meta diff --git a/packages/spacetimedb.bsatn.runtime/0.12.0/lib/netstandard2.1.meta b/packages/spacetimedb.bsatn.runtime/1.0.0/lib/netstandard2.1.meta similarity index 100% rename from packages/spacetimedb.bsatn.runtime/0.12.0/lib/netstandard2.1.meta rename to packages/spacetimedb.bsatn.runtime/1.0.0/lib/netstandard2.1.meta diff --git a/packages/spacetimedb.bsatn.runtime/1.0.0/lib/netstandard2.1/SpacetimeDB.BSATN.Runtime.dll b/packages/spacetimedb.bsatn.runtime/1.0.0/lib/netstandard2.1/SpacetimeDB.BSATN.Runtime.dll new file mode 100644 index 0000000000000000000000000000000000000000..329765a8ece422bbc4ae14cf2818ae5e07beb858 GIT binary patch literal 64512 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 literal 0 HcmV?d00001 diff --git a/packages/spacetimedb.bsatn.runtime/0.12.0/lib/netstandard2.1/SpacetimeDB.BSATN.Runtime.dll.meta b/packages/spacetimedb.bsatn.runtime/1.0.0/lib/netstandard2.1/SpacetimeDB.BSATN.Runtime.dll.meta similarity index 100% rename from packages/spacetimedb.bsatn.runtime/0.12.0/lib/netstandard2.1/SpacetimeDB.BSATN.Runtime.dll.meta rename to packages/spacetimedb.bsatn.runtime/1.0.0/lib/netstandard2.1/SpacetimeDB.BSATN.Runtime.dll.meta From 8ee021e0bae8ff19b85c6122c0e459ebcd240ac0 Mon Sep 17 00:00:00 2001 From: Zeke Foppa <bfops@users.noreply.github.com> Date: Tue, 29 Oct 2024 13:32:22 -0700 Subject: [PATCH 33/55] Update DLLs to release/v1.0.0-rc1 --- .../dotnet/cs/SpacetimeDB.BSATN.Codegen.dll | Bin 57344 -> 57344 bytes .../SpacetimeDB.BSATN.Runtime.dll | Bin 64512 -> 64512 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/packages/spacetimedb.bsatn.runtime/1.0.0/analyzers/dotnet/cs/SpacetimeDB.BSATN.Codegen.dll b/packages/spacetimedb.bsatn.runtime/1.0.0/analyzers/dotnet/cs/SpacetimeDB.BSATN.Codegen.dll index bcbcf0ef5a8048fa49abe3ed5f29c7e968896c71..d25daddb24b51168c1c9b0f330b3047d0d54c689 100644 GIT binary patch delta 238 zcmZoTz}#?vc|r%v<jo<+H}?G4E^zYiGRr%8&%F9J%-box|4aSmDLcv}HH^{>(kv5A zjgpO%63r}(Eld;5lMRdwEG<(^Qj<)~%uSNb(~=U?j5Zrx`o_#sAG&(;WSwgP0&3C* zeKk_;p51@cnhl-5l~3-uwp0Ns_|h9H2vVJXNB-ElHRjQ46=(Ww4!kbS>Tkr5#$W)% zmJEpurVK_5$qdE}NkFz4g9VUf0TfGQFb9en0C@&LRhA4X3?>YzKs6>nHReDv8Azsq O^`tQvZQgyyl^Fo%@=vt@ delta 238 zcmZoTz}#?vc|r#ZPlf06jXgiM3#eK=U9IW7-euFhJBccvJI-#NvZG8=Bhf6;EZHQ@ zz#=Wh!qOnsASu-%IngNDG||#BIXTHZ)i}{4(ZVp<Y_rj&Z_F%;FV#*>*0~lSFp*m) zb&|QU<DPtzPkyVS_$K#UTdDvRyjTMj1giF8{XRkS*p7<Q&5GWaHwRvqX7x{GFk=AW zWCjz4GzJ3(3m};SBrSpLR3J_Qk`_SOL<S=uX$qt*fiM{elNii_a>hUr6Ck!=Fa(O3 KZQgyyl^FmRmrb1j diff --git a/packages/spacetimedb.bsatn.runtime/1.0.0/lib/netstandard2.1/SpacetimeDB.BSATN.Runtime.dll b/packages/spacetimedb.bsatn.runtime/1.0.0/lib/netstandard2.1/SpacetimeDB.BSATN.Runtime.dll index 329765a8ece422bbc4ae14cf2818ae5e07beb858..96670361095fbd462e5d19ef78c05f770ee2ac24 100644 GIT binary patch delta 238 zcmZqp!QAkJc|r%v`a{<@Z|qreLts0{I;%B3O3qF7P79s~<#26wxLGEmVU%W&W|?Se zlx&=oXl7w-VVY>3Y+!6)X_;b@nq*>TZjx-CmXw%gwE5oKTg)ux`QMLB-t#d)V6yJo ztxOYpyypM7c4w{Bt6!64J}p&%3QEaC1%awvV&41_{=d@hu6FY;-pvm`1+)4aF{CjV z0I?-QB7-S|5koSAF+&oNZN^{$WLW^k5*f^aq6R>o0Z^4CLkfclLn=^>2~dqWkW2=W QX<$8R3`U!qf7CJq0H@wn6aWAK delta 238 zcmZqp!QAkJc|r$ElG6358+(@A5RmAIR^e~tbK09)^s6i&eCB3{n`I&ziDrpr$tGzA z7HKIKmIkQ?NvRgeiAKq$iI$eh$w}s^#)&407KX`YoA157#mw?b_UG=&dp-sT+<qTr z$nkxR`z(zWd(5(z-kdD+X{iEKaJdvz5TttU(piOF_73?So}5QFKl~KT>YvD9#sI>} z3?>X|3<eAqKr#hLS_0XrK%4|5Er7C#3`RiG6i8bFVKNXVF_;78jDaF1Ky1Na2oy8h J-29`K830%(Q)2)C From d687f5620df12cb0b562ebfe9ded22a55993b39e Mon Sep 17 00:00:00 2001 From: SteveGibson <100594800+SteveBoytsun@users.noreply.github.com> Date: Wed, 30 Oct 2024 12:33:54 -0400 Subject: [PATCH 34/55] Fixed subscription updates not clearing tables with no subscribed values (#182) ## Description of Changes *Describe what has been changed, any new features or bug fixes* Context: https://discord.com/channels/568217153853980682/669989878955638785/1301132060878049332 Currently, when we receive subscription updates, a table will only be diffed if subscription has any rows for that table. If, however, there are no subscribed values, that table will NOT be diffed, and therefore will not get cleared. Values from previous subscription will still be there, so the table is in incorrect state. This PR fixes that by making sure that ALL tables are checked in state diff ## API - [ ] This is an API breaking change to the SDK *If the API is breaking, please state below what will break* ## Requires SpacetimeDB PRs *List any PRs here that are required for this SDK change to work* ## Testing *Write instructions for a test that you performed for this PR* - [ ] Create a table A with an `val: i32` field - [ ] Generate rows for table A with field values in the range (0..100) - [ ] Connect client and subscribe to `SELECT * FROM A WHERE val > 0` (will have all rows from table A) - [ ] Change your subscription to `SELECT * FROM A WHERE val > 1000` (should have no rows) - [ ] Note that after subscription is applied, table A will have no values Co-authored-by: Steve Boytsun <steve@clockwokrlabs.io> --- src/SpacetimeDBClient.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/SpacetimeDBClient.cs b/src/SpacetimeDBClient.cs index dc627f2f..ae9476ab 100644 --- a/src/SpacetimeDBClient.cs +++ b/src/SpacetimeDBClient.cs @@ -547,7 +547,10 @@ ProcessedMessage CalculateStateDiff(PreProcessedMessage preProcessedMessage) { if (!subscriptionInserts.TryGetValue(table.ClientTableType, out var hashSet)) { - continue; + // We still need to process tables that weren't included in subscription. + // Otherwise we won't delete no-longer-available values + hashSet = new HashSet<byte[]>(); + subscriptionInserts.Add(table.ClientTableType, hashSet); } foreach (var (rowBytes, oldValue) in table.IterEntries().Where(kv => !hashSet.Contains(kv.Key))) From b533b1d9a28b215a6e15753d6b1480e18b2f6764 Mon Sep 17 00:00:00 2001 From: Jeremie Pelletier <jeremiep@gmail.com> Date: Wed, 30 Oct 2024 14:45:05 -0400 Subject: [PATCH 35/55] Quickstart client fix --- examples~/quickstart/client/Program.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/examples~/quickstart/client/Program.cs b/examples~/quickstart/client/Program.cs index 8327d976..9f478f3c 100644 --- a/examples~/quickstart/client/Program.cs +++ b/examples~/quickstart/client/Program.cs @@ -137,11 +137,7 @@ void OnConnect(DbConnection conn, Identity identity, string authToken) conn.SubscriptionBuilder() .OnApplied(OnSubscriptionApplied) - .Subscribe("SELECT * FROM User"); - - conn.SubscriptionBuilder() - .OnApplied(OnSubscriptionApplied) - .Subscribe("SELECT * FROM Message"); + .Subscribe("SELECT * FROM user", "SELECT * FROM message"); } void OnConnectError(WebSocketError? error, string message) From 53d8a926df5a76af533d3e61544f1f5278107ff5 Mon Sep 17 00:00:00 2001 From: John Detter <4099508+jdetter@users.noreply.github.com> Date: Wed, 30 Oct 2024 22:08:36 +0000 Subject: [PATCH 36/55] Fix Connection Error: Success (#166) ## Description of Changes This PR edits the handling of errors related to websocket connections and disconnections. In particular, clients and users would often run into the dreaded `Connection Error: Success` message which was confusing and frustrating. This PR better addresses the error by providing more guidance and debug info for the user. It is unfortunately still suboptimal because the `HttpStatusCode` is not available in the .NET core version that Unity supports. We try to be as helpful as possible in this scenario. ## API - [x] This is an API breaking change to the SDK, because it changes the returned values from the `OnDisconnect` and `OnConnectError` callbacks to implement the API specification: https://github.com/clockworklabs/SpacetimeDBPrivate/pull/866/files#diff-be533cc04817c33605a68d717c6ec320c4449904266ee8e1096971e9e17e8d31R424 ## Requires SpacetimeDB PRs No changes to SpacetimeDB required. ## Testing I, Tyler, have tested this and confirmed it to be working with CircleGame. Here is a sample of the output in the case of `Connection Error: Success`: <img width="1324" alt="image" src="https://github.com/user-attachments/assets/2b98c69f-07e2-4d0b-a61f-0ae4f84d62f6"> --------- Co-authored-by: John Detter <no-reply@boppygames.gg> Co-authored-by: Tyler Cloutier <cloutiertyler@aol.com> Co-authored-by: Zeke Foppa <bfops@users.noreply.github.com> --- examples~/quickstart/client/Program.cs | 4 +- src/SpacetimeDBClient.cs | 10 +- src/WebSocket.cs | 132 ++++++++++++++++++++++--- 3 files changed, 123 insertions(+), 23 deletions(-) diff --git a/examples~/quickstart/client/Program.cs b/examples~/quickstart/client/Program.cs index 8327d976..f6978ff6 100644 --- a/examples~/quickstart/client/Program.cs +++ b/examples~/quickstart/client/Program.cs @@ -144,12 +144,12 @@ void OnConnect(DbConnection conn, Identity identity, string authToken) .Subscribe("SELECT * FROM Message"); } -void OnConnectError(WebSocketError? error, string message) +void OnConnectError(Exception e) { } -void OnDisconnect(DbConnection conn, WebSocketCloseStatus? status, WebSocketError? error) +void OnDisconnect(DbConnection conn, Exception? e) { } diff --git a/src/SpacetimeDBClient.cs b/src/SpacetimeDBClient.cs index ae9476ab..40f8d0cc 100644 --- a/src/SpacetimeDBClient.cs +++ b/src/SpacetimeDBClient.cs @@ -64,15 +64,15 @@ public DbConnectionBuilder<DbConnection, Reducer> OnConnect(Action<DbConnection, return this; } - public DbConnectionBuilder<DbConnection, Reducer> OnConnectError(Action<WebSocketError?, string> cb) + public DbConnectionBuilder<DbConnection, Reducer> OnConnectError(Action<Exception> cb) { - conn.webSocket.OnConnectError += (a, b) => cb.Invoke(a, b); + conn.webSocket.OnConnectError += (e) => cb.Invoke(e); return this; } - public DbConnectionBuilder<DbConnection, Reducer> OnDisconnect(Action<DbConnection, WebSocketCloseStatus?, WebSocketError?> cb) + public DbConnectionBuilder<DbConnection, Reducer> OnDisconnect(Action<DbConnection, Exception?> cb) { - conn.webSocket.OnClose += (code, error) => cb.Invoke(conn, code, error); + conn.webSocket.OnClose += (e) => cb.Invoke(conn, e); return this; } } @@ -156,7 +156,7 @@ protected DbConnectionBase() webSocket.OnMessage += OnMessageReceived; webSocket.OnSendError += a => onSendError?.Invoke(a); #if UNITY_5_3_OR_NEWER - webSocket.OnClose += (a, b) => SpacetimeDBNetworkManager.ActiveConnections.Remove(this); + webSocket.OnClose += (e) => SpacetimeDBNetworkManager.ActiveConnections.Remove(this); #endif networkMessageProcessThread = new Thread(PreProcessMessages); diff --git a/src/WebSocket.cs b/src/WebSocket.cs index cd22c26f..78247b44 100644 --- a/src/WebSocket.cs +++ b/src/WebSocket.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Concurrent; using System.Linq; +using System.Net.Sockets; using System.Net.WebSockets; using System.Text; using System.Threading; @@ -17,9 +18,9 @@ internal class WebSocket public delegate void MessageEventHandler(byte[] message, DateTime timestamp); - public delegate void CloseEventHandler(WebSocketCloseStatus? code, WebSocketError? error); + public delegate void CloseEventHandler(Exception? e); - public delegate void ConnectErrorEventHandler(WebSocketError? error, string message); + public delegate void ConnectErrorEventHandler(Exception e); public delegate void SendErrorEventHandler(Exception e); public struct ConnectOptions @@ -70,23 +71,81 @@ public async Task Connect(string? auth, string host, string nameOrAddress, Addre try { await Ws.ConnectAsync(url, source.Token); - if (OnConnect != null) dispatchQueue.Enqueue(() => OnConnect()); + if (Ws.State == WebSocketState.Open) + { + if (OnConnect != null) + { + dispatchQueue.Enqueue(() => OnConnect()); + } + } + else + { + if (OnConnectError != null) + { + dispatchQueue.Enqueue(() => OnConnectError( + new Exception($"WebSocket connection failed. Current state: {Ws.State}"))); + } + return; + } + } + catch (WebSocketException ex) when (ex.WebSocketErrorCode == WebSocketError.Success) + { + // How can we get here: + // - When you go to connect and the server isn't running (port closed) - target machine actively refused + // - 404 - No module with at that module address instead of 101 upgrade + // - 401? - When the identity received by SpacetimeDB wasn't signed by its signing key + // - 400 - When the auth is malformed + if (OnConnectError != null) + { + // .net 6,7,8 has support for Ws.HttpStatusCode as long as you set + // ClientWebSocketOptions.CollectHttpResponseDetails = true + var message = "A WebSocketException occurred, even though the WebSocketErrorCode is \"Success\".\n" + + "This indicates that there was no native error information for the exception.\n" + + "Due to limitations in the .NET core version we do not have access to the HTTP status code returned by the request which would provide more info on the nature of the error.\n\n" + + "This error could arise for a number of reasons:\n" + + "1. The target machine actively refused the connection.\n" + + "2. The module you are trying to connect to does not exist (404 NOT FOUND).\n" + + "3. The auth token you sent to SpacetimeDB was not signed by the correct signing key (400 BAD REQUEST).\n" + + "4. The auth token is malformed (400 BAD REQUEST).\n" + + "5. You are not authorized (401 UNAUTHORIZED).\n\n" + + "Did you forget to start the server or publish your module?\n\n" + + "Here are some values that might help you debug:\n" + + $"Message: {ex.Message}\n" + + $"WebSocketErrorCode: {ex.WebSocketErrorCode}\n" + + $"ErrorCode: {ex.ErrorCode}\n" + + $"NativeErrorCode: {ex.NativeErrorCode}\n" + + $"InnerException Message: {ex.InnerException?.Message}\n" + + $"WebSocket CloseStatus: {Ws.CloseStatus}\n" + + $"WebSocket State: {Ws.State}\n" + + $"InnerException: {ex.InnerException}\n" + + $"Exception: {ex}" + ; + dispatchQueue.Enqueue(() => OnConnectError(new Exception(message))); + } + } + catch (WebSocketException ex) + { + if (OnConnectError != null) + { + var message = $"WebSocket connection failed: {ex.WebSocketErrorCode}\n" + + $"Exception message: {ex.Message}\n"; + dispatchQueue.Enqueue(() => OnConnectError(new Exception(message))); + } + } + catch (SocketException ex) + { + // This might occur if the server is unreachable or the DNS lookup fails. + if (OnConnectError != null) + { + dispatchQueue.Enqueue(() => OnConnectError(ex)); + } } catch (Exception ex) { - Log.Exception(ex); if (OnConnectError != null) { - var message = ex.Message; - var code = (ex as WebSocketException)?.WebSocketErrorCode; - if (code == WebSocketError.NotAWebSocket) - { - // not a websocket happens when there is no module published under the address specified - message += " Did you forget to publish your module?"; - } - dispatchQueue.Enqueue(() => OnConnectError(code, message)); + dispatchQueue.Enqueue(() => OnConnectError(ex)); } - return; } while (Ws.State == WebSocketState.Open) @@ -102,7 +161,45 @@ public async Task Connect(string? auth, string host, string nameOrAddress, Addre await Ws.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); } - if (OnClose != null) dispatchQueue.Enqueue(() => OnClose(receiveResult.CloseStatus, null)); + if (OnClose != null) + { + switch (receiveResult.CloseStatus) + { + case WebSocketCloseStatus.NormalClosure: + dispatchQueue.Enqueue(() => OnClose(null)); + break; + case WebSocketCloseStatus.EndpointUnavailable: + dispatchQueue.Enqueue(() => OnClose(new Exception("(1000) The connection has closed after the request was fulfilled."))); + break; + case WebSocketCloseStatus.ProtocolError: + dispatchQueue.Enqueue(() => OnClose(new Exception("(1002) The client or server is terminating the connection because of a protocol error."))); + break; + case WebSocketCloseStatus.InvalidMessageType: + dispatchQueue.Enqueue(() => OnClose(new Exception("(1003) The client or server is terminating the connection because it cannot accept the data type it received."))); + break; + case WebSocketCloseStatus.Empty: + dispatchQueue.Enqueue(() => OnClose(new Exception("(1005) No error specified."))); + break; + case WebSocketCloseStatus.InvalidPayloadData: + dispatchQueue.Enqueue(() => OnClose(new Exception("(1007) The client or server is terminating the connection because it has received data inconsistent with the message type."))); + break; + case WebSocketCloseStatus.PolicyViolation: + dispatchQueue.Enqueue(() => OnClose(new Exception("(1008) The connection will be closed because an endpoint has received a message that violates its policy."))); + break; + case WebSocketCloseStatus.MessageTooBig: + dispatchQueue.Enqueue(() => OnClose(new Exception("(1009) Message too big"))); + break; + case WebSocketCloseStatus.MandatoryExtension: + dispatchQueue.Enqueue(() => OnClose(new Exception("(1010) The client is terminating the connection because it expected the server to negotiate an extension."))); + break; + case WebSocketCloseStatus.InternalServerError: + dispatchQueue.Enqueue(() => OnClose(new Exception("(1011) The connection will be closed by the server because of an error on the server."))); + break; + default: + dispatchQueue.Enqueue(() => OnClose(new Exception("Unknown error"))); + break; + } + } return; } @@ -116,7 +213,10 @@ await Ws.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, var closeMessage = $"Maximum message size: {MAXMessageSize} bytes."; await Ws.CloseAsync(WebSocketCloseStatus.MessageTooBig, closeMessage, CancellationToken.None); - if (OnClose != null) dispatchQueue.Enqueue(() => OnClose(WebSocketCloseStatus.MessageTooBig, null)); + if (OnClose != null) + { + dispatchQueue.Enqueue(() => OnClose(new Exception("(1009) Message too big"))); + } return; } @@ -134,7 +234,7 @@ await Ws.CloseAsync(WebSocketCloseStatus.MessageTooBig, closeMessage, } catch (WebSocketException ex) { - if (OnClose != null) dispatchQueue.Enqueue(() => OnClose(null, ex.WebSocketErrorCode)); + if (OnClose != null) dispatchQueue.Enqueue(() => OnClose(ex)); return; } } From bac557c446b89211683d7922b14a910476c54a47 Mon Sep 17 00:00:00 2001 From: John Detter <4099508+jdetter@users.noreply.github.com> Date: Thu, 31 Oct 2024 17:25:28 +0000 Subject: [PATCH 37/55] Unity testsuite as a git submodule (#186) **Please do not rebase this PR** ## Description of Changes *Describe what has been changed, any new features or bug fixes* This is very similar to https://github.com/clockworklabs/com.clockworklabs.spacetimedbsdk/pull/176 except it imports the circle game as a submodule instead of copying the code over into this repo. This is the SpacetimeDBCircleGame PR that we're dependent on right now: https://github.com/clockworklabs/SpacetimeDBCircleGame/pull/3 - This PR introduces a testsuite which runs in Unity. Right now it just spawns in a circle, eats some food and verifies the decay logic is working correctly. I've also written some reconnection tests but they don't work because reconnections are currently broken. There are also one-off tests but those don't work either because they also require reconnections to be working. Update: reconnections have been fixed via https://github.com/clockworklabs/com.clockworklabs.spacetimedbsdk/pull/168. I've used the built-in unity testsuite framework to achieve this, along with the UnityCI tool from GameCI. The documentation for this docker container can be found here: https://game.ci/docs/github/getting-started/ ## API - [ ] This is an API breaking change to the SDK *If the API is breaking, please state below what will break* Not breaking ## Requires SpacetimeDB PRs *List any PRs here that are required for this SDK change to work* None ## Testsuite SpacetimeDB branch name: 0935b7346b825b8cbb9f36d9ed256136b73b5f0b ## Testing *Write instructions for a test that you performed for this PR* - [x] The testsuite is passing: https://github.com/clockworklabs/com.clockworklabs.spacetimedbsdk/actions/runs/11604456943/job/32313229775 You can follow test instructions here to double check my work: https://github.com/clockworklabs/SpacetimeDBCircleGame/pull/3 ## Follow-up Actions - [ ] Rebase the reconnection logic PR onto this PR and re-enable the reconnection tests --------- Co-authored-by: John Detter <no-reply@boppygames.gg> --- .github/pull_request_template.md | 11 +- .github/workflows/unity-test.yml | 149 ++++++++++++++++++ .../workflows/unity-testsuite-bindings.yml | 120 ++++++++++++++ .gitmodules | 3 + unity-tests~ | 1 + 5 files changed, 283 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/unity-test.yml create mode 100644 .github/workflows/unity-testsuite-bindings.yml create mode 100644 .gitmodules create mode 160000 unity-tests~ diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 88fcaef9..71d6531a 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -7,6 +7,15 @@ *If the API is breaking, please state below what will break* - ## Requires SpacetimeDB PRs *List any PRs here that are required for this SDK change to work* + +## Testsuite +*If you would like to run against a specific SpacetimeDB branch in the testsuite, specify that here. This can be a branch name or a link to a PR.* + +SpacetimeDB branch name: master + +## Testing +*Write instructions for a test that you performed for this PR* + +- [ ] Describe a test for this PR that you have completed diff --git a/.github/workflows/unity-test.yml b/.github/workflows/unity-test.yml new file mode 100644 index 00000000..774fd847 --- /dev/null +++ b/.github/workflows/unity-test.yml @@ -0,0 +1,149 @@ +name: Unity Test Suite + +on: + push: + branches: + - staging + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Checkout submodule + run: | + git submodule init + git submodule update + cd unity-tests~ + git checkout jdetter/circle-game-testsuite + # Grab the branch name from the PR description. If it's not found, master will be used instead. + - name: Extract SpacetimeDB branch name or PR link from PR description + id: extract-branch + if: github.event_name == 'pull_request' + run: | + description=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ + ${{ github.event.pull_request.url }} | jq -r '.body') + + # Check if description contains a branch name or a PR link + branch_or_pr=$(echo "$description" | grep -oP '(?<=SpacetimeDB branch name:\s).+') + echo "Branch or PR found: $branch_or_pr" + + if [[ -z "$branch_or_pr" ]]; then + branch="master" + elif [[ "$branch_or_pr" =~ ^https://github.com/.*/pull/[0-9]+$ ]]; then + # If it's a PR link, extract the branch name from the PR + pr_number=$(echo "$branch_or_pr" | grep -oP '[0-9]+$') + branch=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ + https://api.github.com/repos/clockworklabs/SpacetimeDB/pulls/$pr_number | jq -r '.head.ref') + else + # It's already a branch name + branch="$branch_or_pr" + fi + + echo "branch=$branch" >> $GITHUB_OUTPUT + echo "Final branch name: $branch" + + - name: Replace com.clockworklabs.spacetimedbsdk in manifest.json + run: | + # Get the branch name from the environment variable + branch_name="${{ github.head_ref }}" + # Replace any reference to com.clockworklabs.spacetimedbsdk with the correct GitHub URL using the current branch + sed -i "s|\"com.clockworklabs.spacetimedbsdk\":.*|\"com.clockworklabs.spacetimedbsdk\": \"https://github.com/clockworklabs/com.clockworklabs.spacetimedbsdk.git#$branch_name\",|" unity-tests~/client/Packages/manifest.json + + cat unity-tests~/client/Packages/manifest.json + - name: Replace spacetimedb dependency in Cargo.toml + run: | + # Get the branch name from the environment variable + branch_name="${{ github.head_ref }}" + # Make sure we're using the correct bindings when building the module TODO + sed -i "s|spacetimedb.*=.*|spacetimedb = \{ path = \"../SpacetimeDB/crates/bindings\" \}|" unity-tests~/server/Cargo.toml + cat unity-tests~/server/Cargo.toml + - name: Install Rust toolchain + run: | + curl https://sh.rustup.rs -sSf | sh -s -- -y + source $HOME/.cargo/env + rustup install stable + rustup default stable + + - name: Cache Cargo target directory + uses: actions/cache@v3 + with: + path: unity-tests~/server/target + key: server-target-SpacetimeDBUnityTestsuite-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('unity-tests~/server/Cargo.lock') }} + restore-keys: | + server-target-SpacetimeDBUnityTestsuite-${{ runner.os }}-${{ runner.arch }}- + server-target-SpacetimeDBUnityTestsuite- + + - name: Cache Cargo registry + uses: actions/cache@v3 + with: + path: ~/.cargo/registry + key: cargo-registry-${{ runner.os }}-${{ hashFiles('unity-tests~/server/Cargo.lock') }} + restore-keys: | + cargo-registry-${{ runner.os }}- + cargo-registry- + + - name: Cache Cargo index + uses: actions/cache@v3 + with: + path: ~/.cargo/git + key: cargo-index-${{ runner.os }}-${{ hashFiles('unity-tests~/server/Cargo.lock') }} + restore-keys: | + cargo-index-${{ runner.os }}- + cargo-index- + + - name: Install SpacetimeDB CLI from specific branch + run: | + cd unity-tests~ + git clone https://github.com/clockworklabs/SpacetimeDB.git + cd SpacetimeDB + # Sanitize the branch name by trimming any newlines or spaces + branch_name=$(echo "${{ steps.extract-branch.outputs.branch }}" | tr -d '[:space:]') + # If the branch name is not found, default to master + if [ -z "$branch_name" ]; then + branch_name="master" + fi + git checkout "$branch_name" + echo "Checked out branch: $branch_name" + cargo build --release -p spacetimedb-cli + sudo mv target/release/spacetime /usr/bin/spacetime + + - name: Generate client bindings + run: | + cd unity-tests~/server + bash ./generate.sh -y + + - name: Start SpacetimeDB + run: | + spacetime start & + disown + + - name: Publish module to SpacetimeDB + run: | + cd unity-tests~/server + bash ./publish.sh + + - uses: actions/cache@v3 + with: + path: unity-tests~/client/Library + key: Library-SpacetimeDBUnityTestsuite-Linux-x86 + restore-keys: | + Library-SpacetimeDBUnityTestsuite- + Library- + + - name: Set up Unity + uses: game-ci/unity-test-runner@v4 + with: + unityVersion: 2022.3.32f1 # Adjust Unity version to a valid tag + projectPath: unity-tests~/client # Path to the Unity project subdirectory + githubToken: ${{ secrets.GITHUB_TOKEN }} + testMode: playmode + useHostNetwork: true + env: + UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} + UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} + UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }} + diff --git a/.github/workflows/unity-testsuite-bindings.yml b/.github/workflows/unity-testsuite-bindings.yml new file mode 100644 index 00000000..1bfee47d --- /dev/null +++ b/.github/workflows/unity-testsuite-bindings.yml @@ -0,0 +1,120 @@ +name: Check Unity Testsuite Bindings + +on: + push: + branches: + - staging + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Checkout submodule + run: | + git submodule init + git submodule update + cd unity-tests~ + git checkout jdetter/circle-game-testsuite + # Grab the branch name from the PR description. If it's not found, master will be used instead. + - name: Extract SpacetimeDB branch name or PR link from PR description + id: extract-branch + if: github.event_name == 'pull_request' + run: | + description=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ + ${{ github.event.pull_request.url }} | jq -r '.body') + + # Check if description contains a branch name or a PR link + branch_or_pr=$(echo "$description" | grep -oP '(?<=SpacetimeDB branch name:\s).+') + echo "Branch or PR found: $branch_or_pr" + + if [[ -z "$branch_or_pr" ]]; then + branch="master" + elif [[ "$branch_or_pr" =~ ^https://github.com/.*/pull/[0-9]+$ ]]; then + # If it's a PR link, extract the branch name from the PR + pr_number=$(echo "$branch_or_pr" | grep -oP '[0-9]+$') + branch=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ + https://api.github.com/repos/clockworklabs/SpacetimeDB/pulls/$pr_number | jq -r '.head.ref') + else + # It's already a branch name + branch="$branch_or_pr" + fi + + echo "branch=$branch" >> $GITHUB_OUTPUT + echo "Final branch name: $branch" + + - name: Replace spacetimedb dependency in Cargo.toml + run: | + # Get the branch name from the environment variable + branch_name="${{ github.head_ref }}" + # Make sure we're using the correct bindings when building the module TODO + sed -i "s|spacetimedb.*=.*|spacetimedb = \{ path = \"../SpacetimeDB/crates/bindings\" \}|" unity-tests~/server/Cargo.toml + cat unity-tests~/server/Cargo.toml + - name: Install Rust toolchain + run: | + curl https://sh.rustup.rs -sSf | sh -s -- -y + source $HOME/.cargo/env + rustup install stable + rustup default stable + + - name: Cache Cargo target directory + uses: actions/cache@v3 + with: + path: unity-tests~/server/target + key: server-target-SpacetimeDBUnityTestsuite-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('unity-tests~/server/Cargo.lock') }} + restore-keys: | + server-target-SpacetimeDBUnityTestsuite-${{ runner.os }}-${{ runner.arch }}- + server-target-SpacetimeDBUnityTestsuite- + + - name: Cache Cargo registry + uses: actions/cache@v3 + with: + path: ~/.cargo/registry + key: cargo-registry-${{ runner.os }}-${{ hashFiles('unity-tests~/server/Cargo.lock') }} + restore-keys: | + cargo-registry-${{ runner.os }}- + cargo-registry- + + - name: Cache Cargo index + uses: actions/cache@v3 + with: + path: ~/.cargo/git + key: cargo-index-${{ runner.os }}-${{ hashFiles('unity-tests~/server/Cargo.lock') }} + restore-keys: | + cargo-index-${{ runner.os }}- + cargo-index- + + - name: Install SpacetimeDB CLI from specific branch + run: | + cd unity-tests~ + git clone https://github.com/clockworklabs/SpacetimeDB.git + cd SpacetimeDB + # Sanitize the branch name by trimming any newlines or spaces + branch_name=$(echo "${{ steps.extract-branch.outputs.branch }}" | tr -d '[:space:]') + # If the branch name is not found, default to master + if [ -z "$branch_name" ]; then + branch_name="master" + fi + git checkout "$branch_name" + echo "Checked out branch: $branch_name" + cargo build --release -p spacetimedb-cli + sudo mv target/release/spacetime /usr/bin/spacetime + + - name: Generate client bindings + run: | + cd unity-tests~/server + bash ./generate.sh -y + + - name: Check for changes + run: | + git diff --exit-code + continue-on-error: true + + - name: Fail if there are changes + if: ${{ steps.check-for-changes.outcome == 'failure' }} + run: | + echo "Error: Bindings are dirty. Please generate bindings again and commit them to this branch." + exit 1 diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..50c54dcb --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "SpacetimeDBCircleGame"] + path = unity-tests~ + url = https://github.com/clockworklabs/SpacetimeDBCircleGame diff --git a/unity-tests~ b/unity-tests~ new file mode 160000 index 00000000..75047b5e --- /dev/null +++ b/unity-tests~ @@ -0,0 +1 @@ +Subproject commit 75047b5e58a67ba8e3b652dab17ddde61b661989 From 9a5670431b1efab8648c69f440e316ac81ea9478 Mon Sep 17 00:00:00 2001 From: Mazdak Farrokhzad <twingoow@gmail.com> Date: Thu, 31 Oct 2024 19:08:52 +0100 Subject: [PATCH 38/55] Add gzip + none compression algos and let SDK pick compression (take 2) (#174) ## Description of Changes Companion to https://github.com/clockworklabs/SpacetimeDB/pull/1802. ## Requires SpacetimeDB PRs None ## Test suite SpacetimeDB branch name: 0935b7346b825b8cbb9f36d9ed256136b73b5f0b --------- Co-authored-by: Jeremie Pelletier <jeremiep@gmail.com> Co-authored-by: Zeke Foppa <bfops@users.noreply.github.com> --- src/Compression.cs | 9 ++++ src/Compression.cs.meta | 11 +++++ .../ClientApi/CompressableQueryUpdate.cs | 3 +- src/SpacetimeDBClient.cs | 48 ++++++++++++------- src/WebSocket.cs | 4 +- 5 files changed, 54 insertions(+), 21 deletions(-) create mode 100644 src/Compression.cs create mode 100644 src/Compression.cs.meta diff --git a/src/Compression.cs b/src/Compression.cs new file mode 100644 index 00000000..8f630f99 --- /dev/null +++ b/src/Compression.cs @@ -0,0 +1,9 @@ +namespace SpacetimeDB +{ + public enum Compression + { + None, + Brotli, + Gzip, + } +} diff --git a/src/Compression.cs.meta b/src/Compression.cs.meta new file mode 100644 index 00000000..eef0325c --- /dev/null +++ b/src/Compression.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 03fdc211a87474eeba9f03495eab491e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/SpacetimeDB/ClientApi/CompressableQueryUpdate.cs b/src/SpacetimeDB/ClientApi/CompressableQueryUpdate.cs index af397a58..1af6fd27 100644 --- a/src/SpacetimeDB/ClientApi/CompressableQueryUpdate.cs +++ b/src/SpacetimeDB/ClientApi/CompressableQueryUpdate.cs @@ -12,6 +12,7 @@ namespace SpacetimeDB.ClientApi [SpacetimeDB.Type] public partial record CompressableQueryUpdate : SpacetimeDB.TaggedEnum<( SpacetimeDB.ClientApi.QueryUpdate Uncompressed, - byte[] Brotli + byte[] Brotli, + byte[] Gzip )>; } diff --git a/src/SpacetimeDBClient.cs b/src/SpacetimeDBClient.cs index 40f8d0cc..7fa8dd6f 100644 --- a/src/SpacetimeDBClient.cs +++ b/src/SpacetimeDBClient.cs @@ -22,6 +22,7 @@ public sealed class DbConnectionBuilder<DbConnection, Reducer> string? uri; string? nameOrAddress; string? token; + Compression? compression; public DbConnection Build() { @@ -33,7 +34,7 @@ public DbConnection Build() { throw new InvalidOperationException("Building DbConnection with a null nameOrAddress. Call WithModuleName() first."); } - conn.Connect(token, uri, nameOrAddress); + conn.Connect(token, uri, nameOrAddress, compression ?? Compression.Brotli); #if UNITY_5_3_OR_NEWER SpacetimeDBNetworkManager.ActiveConnections.Add(conn); #endif @@ -58,6 +59,12 @@ public DbConnectionBuilder<DbConnection, Reducer> WithCredentials(in (Identity i return this; } + public DbConnectionBuilder<DbConnection, Reducer> WithCompression(Compression compression) + { + this.compression = compression; + return this; + } + public DbConnectionBuilder<DbConnection, Reducer> OnConnect(Action<DbConnection, Identity, string> cb) { conn.onConnect += (identity, token) => cb.Invoke(conn, identity, token); @@ -209,6 +216,17 @@ enum CompressionAlgos : byte { None = 0, Brotli = 1, + Gzip = 2, + } + + private static BinaryReader BrotliReader(Stream stream) + { + return new BinaryReader(new BrotliStream(stream, CompressionMode.Decompress)); + } + + private static BinaryReader GzipReader(Stream stream) + { + return new BinaryReader(new GZipStream(stream, CompressionMode.Decompress)); } private static ServerMessage DecompressDecodeMessage(byte[] bytes) @@ -221,16 +239,11 @@ private static ServerMessage DecompressDecodeMessage(byte[] bytes) switch (compression) { case CompressionAlgos.None: - { - using var binaryReader = new BinaryReader(stream); - return new ServerMessage.BSATN().Read(binaryReader); - } + return new ServerMessage.BSATN().Read(new BinaryReader(stream)); case CompressionAlgos.Brotli: - { - using var decompressedStream = new BrotliStream(stream, CompressionMode.Decompress); - using var binaryReader = new BinaryReader(decompressedStream); - return new ServerMessage.BSATN().Read(binaryReader); - } + return new ServerMessage.BSATN().Read(BrotliReader(stream)); + case CompressionAlgos.Gzip: + return new ServerMessage.BSATN().Read(GzipReader(stream)); default: throw new InvalidOperationException("Unknown compression type"); } @@ -244,12 +257,11 @@ private static QueryUpdate DecompressDecodeQueryUpdate(CompressableQueryUpdate u return qu; case CompressableQueryUpdate.Brotli(var bytes): - { - using var stream = new MemoryStream(bytes); - using var decompressedStream = new BrotliStream(stream, CompressionMode.Decompress); - using var binaryReader = new BinaryReader(decompressedStream); - return new QueryUpdate.BSATN().Read(binaryReader); - } + return new QueryUpdate.BSATN().Read(BrotliReader(new MemoryStream(bytes))); + + case CompressableQueryUpdate.Gzip(var bytes): + return new QueryUpdate.BSATN().Read(GzipReader(new MemoryStream(bytes))); + default: throw new InvalidOperationException(); } @@ -582,7 +594,7 @@ public void Disconnect() /// </summary> /// <param name="uri"> URI of the SpacetimeDB server (ex: https://testnet.spacetimedb.com) /// <param name="addressOrName">The name or address of the database to connect to</param> - internal void Connect(string? token, string uri, string addressOrName) + internal void Connect(string? token, string uri, string addressOrName, Compression compression) { isClosing = false; @@ -600,7 +612,7 @@ internal void Connect(string? token, string uri, string addressOrName) { try { - await webSocket.Connect(token, uri, addressOrName, Address); + await webSocket.Connect(token, uri, addressOrName, Address, compression); } catch (Exception e) { diff --git a/src/WebSocket.cs b/src/WebSocket.cs index 78247b44..86f47c70 100644 --- a/src/WebSocket.cs +++ b/src/WebSocket.cs @@ -51,9 +51,9 @@ public WebSocket(ConnectOptions options) public bool IsConnected { get { return Ws != null && Ws.State == WebSocketState.Open; } } - public async Task Connect(string? auth, string host, string nameOrAddress, Address clientAddress) + public async Task Connect(string? auth, string host, string nameOrAddress, Address clientAddress, Compression compression) { - var url = new Uri($"{host}/database/subscribe/{nameOrAddress}?client_address={clientAddress}"); + var url = new Uri($"{host}/database/subscribe/{nameOrAddress}?client_address={clientAddress}&compression={compression}"); Ws.Options.AddSubProtocol(_options.Protocol); var source = new CancellationTokenSource(10000); From 0c590a8db34deba68079c6cb9981a3611081c3ab Mon Sep 17 00:00:00 2001 From: Mazdak Farrokhzad <twingoow@gmail.com> Date: Tue, 15 Oct 2024 11:43:49 +0200 Subject: [PATCH 39/55] execute gen-client-api.sh against centril/websocket-light support TransactionUpdateLight support SetReducerFlags --- .../_Globals/SpacetimeDBClient.cs | 28 +- src/CallReducerFlags.cs | 9 + src/SpacetimeDB/ClientApi/BsatnRowList.cs | 1 - src/SpacetimeDB/ClientApi/CallReducer.cs | 7 +- src/SpacetimeDB/ClientApi/OneOffQuery.cs | 1 - .../ClientApi/OneOffQueryResponse.cs | 1 - src/SpacetimeDB/ClientApi/QueryUpdate.cs | 1 - src/SpacetimeDB/ClientApi/ReducerCallInfo.cs | 1 - src/SpacetimeDB/ClientApi/ServerMessage.cs | 1 + .../ClientApi/TransactionUpdateLight.cs | 38 ++ src/SpacetimeDBClient.cs | 330 ++++++++++-------- src/WebSocket.cs | 9 +- ...otTests.VerifyAllTablesParsed.verified.txt | 17 +- tools~/gen-client-api.sh | 0 14 files changed, 285 insertions(+), 159 deletions(-) create mode 100644 src/CallReducerFlags.cs create mode 100644 src/SpacetimeDB/ClientApi/TransactionUpdateLight.cs mode change 100644 => 100755 tools~/gen-client-api.sh diff --git a/examples~/quickstart/client/module_bindings/_Globals/SpacetimeDBClient.cs b/examples~/quickstart/client/module_bindings/_Globals/SpacetimeDBClient.cs index c0fb266b..726f56d0 100644 --- a/examples~/quickstart/client/module_bindings/_Globals/SpacetimeDBClient.cs +++ b/examples~/quickstart/client/module_bindings/_Globals/SpacetimeDBClient.cs @@ -63,13 +63,14 @@ internal UserHandle() public sealed class RemoteReducers : RemoteBase<DbConnection> { - internal RemoteReducers(DbConnection conn) : base(conn) {} + internal RemoteReducers(DbConnection conn, SetReducerFlags SetReducerFlags) : base(conn) { this.SetCallReducerFlags = SetReducerFlags; } + internal readonly SetReducerFlags SetCallReducerFlags; public delegate void SendMessageHandler(EventContext ctx, string text); public event SendMessageHandler? OnSendMessage; public void SendMessage(string text) { - conn.InternalCallReducer(new SendMessage { Text = text }); + conn.InternalCallReducer(new SendMessage { Text = text }, this.SetCallReducerFlags.SendMessageFlags); } public bool InvokeSendMessage(EventContext ctx, SendMessage args) @@ -86,7 +87,7 @@ public bool InvokeSendMessage(EventContext ctx, SendMessage args) public void SetName(string name) { - conn.InternalCallReducer(new SetName { Name = name }); + conn.InternalCallReducer(new SetName { Name = name }, this.SetCallReducerFlags.SetNameFlags); } public bool InvokeSetName(EventContext ctx, SetName args) @@ -100,14 +101,25 @@ public bool InvokeSetName(EventContext ctx, SetName args) } } + public sealed class SetReducerFlags + { + internal SetReducerFlags() { } + internal CallReducerFlags SendMessageFlags; + public void SendMessage(CallReducerFlags flags) { this.SendMessageFlags = flags; } + internal CallReducerFlags SetNameFlags; + public void SetName(CallReducerFlags flags) { this.SetNameFlags = flags; } + } + public partial record EventContext : DbContext<RemoteTables>, IEventContext { public readonly RemoteReducers Reducers; + public readonly SetReducerFlags SetReducerFlags; public readonly Event<Reducer> Event; internal EventContext(DbConnection conn, Event<Reducer> reducerEvent) : base(conn.Db) { Reducers = conn.Reducers; + SetReducerFlags = conn.SetReducerFlags; Event = reducerEvent; } } @@ -124,10 +136,12 @@ public class DbConnection : DbConnectionBase<DbConnection, Reducer> { public readonly RemoteTables Db = new(); public readonly RemoteReducers Reducers; + public readonly SetReducerFlags SetReducerFlags; public DbConnection() { - Reducers = new(this); + SetReducerFlags = new(); + Reducers = new(this, this.SetReducerFlags); clientDB.AddTable<Message>("message", Db.Message); clientDB.AddTable<User>("user", Db.User); @@ -136,7 +150,8 @@ public DbConnection() protected override Reducer ToReducer(TransactionUpdate update) { var encodedArgs = update.ReducerCall.Args; - return update.ReducerCall.ReducerName switch { + return update.ReducerCall.ReducerName switch + { "send_message" => new Reducer.SendMessage(BSATNHelpers.Decode<SendMessage>(encodedArgs)), "set_name" => new Reducer.SetName(BSATNHelpers.Decode<SetName>(encodedArgs)), "<none>" => new Reducer.StdbNone(default), @@ -153,7 +168,8 @@ protected override IEventContext ToEventContext(Event<Reducer> reducerEvent) => protected override bool Dispatch(IEventContext context, Reducer reducer) { var eventContext = (EventContext)context; - return reducer switch { + return reducer switch + { Reducer.SendMessage(var args) => Reducers.InvokeSendMessage(eventContext, args), Reducer.SetName(var args) => Reducers.InvokeSetName(eventContext, args), Reducer.StdbNone or diff --git a/src/CallReducerFlags.cs b/src/CallReducerFlags.cs new file mode 100644 index 00000000..0a3ea363 --- /dev/null +++ b/src/CallReducerFlags.cs @@ -0,0 +1,9 @@ +namespace SpacetimeDB +{ + public enum CallReducerFlags : byte + { + // This is the default. + FullUpdate = 0, + NoSuccessNotify = 1, + } +} diff --git a/src/SpacetimeDB/ClientApi/BsatnRowList.cs b/src/SpacetimeDB/ClientApi/BsatnRowList.cs index 5df43d6b..1f17c790 100644 --- a/src/SpacetimeDB/ClientApi/BsatnRowList.cs +++ b/src/SpacetimeDB/ClientApi/BsatnRowList.cs @@ -7,7 +7,6 @@ using System; using SpacetimeDB; using System.Collections.Generic; -using System.Linq; using System.Runtime.Serialization; namespace SpacetimeDB.ClientApi diff --git a/src/SpacetimeDB/ClientApi/CallReducer.cs b/src/SpacetimeDB/ClientApi/CallReducer.cs index 7c3ce90e..212d9bb3 100644 --- a/src/SpacetimeDB/ClientApi/CallReducer.cs +++ b/src/SpacetimeDB/ClientApi/CallReducer.cs @@ -7,7 +7,6 @@ using System; using SpacetimeDB; using System.Collections.Generic; -using System.Linq; using System.Runtime.Serialization; namespace SpacetimeDB.ClientApi @@ -22,16 +21,20 @@ public partial class CallReducer public byte[] Args; [DataMember(Name = "request_id")] public uint RequestId; + [DataMember(Name = "flags")] + public byte Flags; public CallReducer( string Reducer, byte[] Args, - uint RequestId + uint RequestId, + byte Flags ) { this.Reducer = Reducer; this.Args = Args; this.RequestId = RequestId; + this.Flags = Flags; } public CallReducer() diff --git a/src/SpacetimeDB/ClientApi/OneOffQuery.cs b/src/SpacetimeDB/ClientApi/OneOffQuery.cs index 5ee2c66d..11370a54 100644 --- a/src/SpacetimeDB/ClientApi/OneOffQuery.cs +++ b/src/SpacetimeDB/ClientApi/OneOffQuery.cs @@ -7,7 +7,6 @@ using System; using SpacetimeDB; using System.Collections.Generic; -using System.Linq; using System.Runtime.Serialization; namespace SpacetimeDB.ClientApi diff --git a/src/SpacetimeDB/ClientApi/OneOffQueryResponse.cs b/src/SpacetimeDB/ClientApi/OneOffQueryResponse.cs index f539516f..2bdb8e1a 100644 --- a/src/SpacetimeDB/ClientApi/OneOffQueryResponse.cs +++ b/src/SpacetimeDB/ClientApi/OneOffQueryResponse.cs @@ -7,7 +7,6 @@ using System; using SpacetimeDB; using System.Collections.Generic; -using System.Linq; using System.Runtime.Serialization; namespace SpacetimeDB.ClientApi diff --git a/src/SpacetimeDB/ClientApi/QueryUpdate.cs b/src/SpacetimeDB/ClientApi/QueryUpdate.cs index 858dea65..25acc28e 100644 --- a/src/SpacetimeDB/ClientApi/QueryUpdate.cs +++ b/src/SpacetimeDB/ClientApi/QueryUpdate.cs @@ -7,7 +7,6 @@ using System; using SpacetimeDB; using System.Collections.Generic; -using System.Linq; using System.Runtime.Serialization; namespace SpacetimeDB.ClientApi diff --git a/src/SpacetimeDB/ClientApi/ReducerCallInfo.cs b/src/SpacetimeDB/ClientApi/ReducerCallInfo.cs index 0ca52252..f8acaffd 100644 --- a/src/SpacetimeDB/ClientApi/ReducerCallInfo.cs +++ b/src/SpacetimeDB/ClientApi/ReducerCallInfo.cs @@ -7,7 +7,6 @@ using System; using SpacetimeDB; using System.Collections.Generic; -using System.Linq; using System.Runtime.Serialization; namespace SpacetimeDB.ClientApi diff --git a/src/SpacetimeDB/ClientApi/ServerMessage.cs b/src/SpacetimeDB/ClientApi/ServerMessage.cs index 46b5be3a..f66fdd98 100644 --- a/src/SpacetimeDB/ClientApi/ServerMessage.cs +++ b/src/SpacetimeDB/ClientApi/ServerMessage.cs @@ -13,6 +13,7 @@ namespace SpacetimeDB.ClientApi public partial record ServerMessage : SpacetimeDB.TaggedEnum<( SpacetimeDB.ClientApi.InitialSubscription InitialSubscription, SpacetimeDB.ClientApi.TransactionUpdate TransactionUpdate, + SpacetimeDB.ClientApi.TransactionUpdateLight TransactionUpdateLight, SpacetimeDB.ClientApi.IdentityToken IdentityToken, SpacetimeDB.ClientApi.OneOffQueryResponse OneOffQueryResponse )>; diff --git a/src/SpacetimeDB/ClientApi/TransactionUpdateLight.cs b/src/SpacetimeDB/ClientApi/TransactionUpdateLight.cs new file mode 100644 index 00000000..3a3ea6b5 --- /dev/null +++ b/src/SpacetimeDB/ClientApi/TransactionUpdateLight.cs @@ -0,0 +1,38 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN RUST INSTEAD. +// <auto-generated /> + +#nullable enable + +using System; +using SpacetimeDB; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace SpacetimeDB.ClientApi +{ + [SpacetimeDB.Type] + [DataContract] + public partial class TransactionUpdateLight + { + [DataMember(Name = "request_id")] + public uint RequestId; + [DataMember(Name = "update")] + public SpacetimeDB.ClientApi.DatabaseUpdate Update; + + public TransactionUpdateLight( + uint RequestId, + SpacetimeDB.ClientApi.DatabaseUpdate Update + ) + { + this.RequestId = RequestId; + this.Update = Update; + } + + public TransactionUpdateLight() + { + this.Update = new(); + } + + } +} diff --git a/src/SpacetimeDBClient.cs b/src/SpacetimeDBClient.cs index 7fa8dd6f..859a3d54 100644 --- a/src/SpacetimeDBClient.cs +++ b/src/SpacetimeDBClient.cs @@ -23,6 +23,7 @@ public sealed class DbConnectionBuilder<DbConnection, Reducer> string? nameOrAddress; string? token; Compression? compression; + bool light; public DbConnection Build() { @@ -34,7 +35,7 @@ public DbConnection Build() { throw new InvalidOperationException("Building DbConnection with a null nameOrAddress. Call WithModuleName() first."); } - conn.Connect(token, uri, nameOrAddress, compression ?? Compression.Brotli); + conn.Connect(token, uri, nameOrAddress, compression ?? Compression.Brotli, light); #if UNITY_5_3_OR_NEWER SpacetimeDBNetworkManager.ActiveConnections.Add(conn); #endif @@ -65,6 +66,12 @@ public DbConnectionBuilder<DbConnection, Reducer> WithCompression(Compression co return this; } + public DbConnectionBuilder<DbConnection, Reducer> WithLightMode(bool light) + { + this.light = light; + return this; + } + public DbConnectionBuilder<DbConnection, Reducer> OnConnect(Action<DbConnection, Identity, string> cb) { conn.onConnect += (identity, token) => cb.Invoke(conn, identity, token); @@ -331,18 +338,28 @@ void PreProcessMessages() } } - PreProcessedMessage PreProcessMessage(UnprocessedMessage unprocessed) + IEnumerable<(IRemoteTableHandle, TableUpdate)> GetTables(DatabaseUpdate updates) { - var dbOps = new List<DbOp>(); + foreach (var update in updates.Tables) + { + var tableName = update.TableName; + var table = clientDB.GetTable(tableName); + if (table == null) + { + Log.Error($"Unknown table name: {tableName}"); + continue; + } - var message = DecompressDecodeMessage(unprocessed.bytes); + yield return (table, update); + } + } - ReducerEvent<Reducer>? reducerEvent = default; + (List<DbOp>, Dictionary<System.Type, HashSet<byte[]>>?) PreProcessInitialSubscription(InitialSubscription initSub) + { + var dbOps = new List<DbOp>(); // This is all of the inserts Dictionary<System.Type, HashSet<byte[]>>? subscriptionInserts = null; - // All row updates that have a primary key, this contains inserts, deletes and updates - var primaryKeyChanges = new Dictionary<(System.Type tableType, object primaryKeyValue), DbOp>(); HashSet<byte[]> GetInsertHashSet(System.Type tableType, int tableSize) { @@ -355,54 +372,168 @@ HashSet<byte[]> GetInsertHashSet(System.Type tableType, int tableSize) return hashSet; } - switch (message) + int cap = initSub.DatabaseUpdate.Tables.Sum(a => (int)a.NumRows); + subscriptionInserts = new(capacity: cap); + + // First apply all of the state + foreach (var (table, update) in GetTables(initSub.DatabaseUpdate)) { - case ServerMessage.InitialSubscription(var initialSubscription): - int cap = initialSubscription.DatabaseUpdate.Tables.Sum(a => (int)a.NumRows); - subscriptionInserts = new(capacity: cap); + var hashSet = GetInsertHashSet(table.ClientTableType, (int)update.NumRows); + + foreach (var cqu in update.Updates) + { + var qu = DecompressDecodeQueryUpdate(cqu); + if (BsatnRowListCount(qu.Deletes) != 0) + { + Log.Warn("Non-insert during a subscription update!"); + } - // First apply all of the state - foreach (var update in initialSubscription.DatabaseUpdate.Tables) + foreach (var bin in BsatnRowListIter(qu.Inserts)) { - var tableName = update.TableName; - var table = clientDB.GetTable(tableName); - if (table == null) + if (!hashSet.Add(bin)) { - Log.Error($"Unknown table name: {tableName}"); + // Ignore duplicate inserts in the same subscription update. continue; } - var hashSet = GetInsertHashSet(table.ClientTableType, (int)update.NumRows); + var obj = table.DecodeValue(bin); + var op = new DbOp + { + table = table, + insert = new(obj, bin), + }; + + dbOps.Add(op); + } + } + } + + return (dbOps, subscriptionInserts); + } + + List<DbOp> PreProcessDatabaseUpdate(DatabaseUpdate updates) + { + var dbOps = new List<DbOp>(); - foreach (var cqu in update.Updates) + // All row updates that have a primary key, this contains inserts, deletes and updates + var primaryKeyChanges = new Dictionary<(System.Type tableType, object primaryKeyValue), DbOp>(); + + // First apply all of the state + foreach (var (table, update) in GetTables(updates)) + { + foreach (var cqu in update.Updates) + { + var qu = DecompressDecodeQueryUpdate(cqu); + foreach (var row in BsatnRowListIter(qu.Inserts)) + { + var op = new DbOp { table = table, insert = Decode(table, row, out var pk) }; + if (pk != null) { - var qu = DecompressDecodeQueryUpdate(cqu); - if (BsatnRowListCount(qu.Deletes) != 0) + // Compound key that we use for lookup. + // Consists of type of the table (for faster comparison that string names) + actual primary key of the row. + var key = (table.ClientTableType, pk); + + if (primaryKeyChanges.TryGetValue(key, out var oldOp)) { - Log.Warn("Non-insert during a subscription update!"); + if ((op.insert is not null && oldOp.insert is not null) || (op.delete is not null && oldOp.delete is not null)) + { + Log.Warn($"Update with the same primary key was applied multiple times! tableName={update.TableName}"); + // TODO(jdetter): Is this a correctable error? This would be a major error on the + // SpacetimeDB side. + continue; + } + + var (insertOp, deleteOp) = op.insert is not null ? (op, oldOp) : (oldOp, op); + op = new DbOp + { + table = insertOp.table, + delete = deleteOp.delete, + insert = insertOp.insert, + }; } + primaryKeyChanges[key] = op; + } + else + { + dbOps.Add(op); + } + } - foreach (var bin in BsatnRowListIter(qu.Inserts)) + foreach (var row in BsatnRowListIter(qu.Deletes)) + { + var op = new DbOp { table = table, delete = Decode(table, row, out var pk) }; + if (pk != null) + { + // Compound key that we use for lookup. + // Consists of type of the table (for faster comparison that string names) + actual primary key of the row. + var key = (table.ClientTableType, pk); + + if (primaryKeyChanges.TryGetValue(key, out var oldOp)) { - if (!hashSet.Add(bin)) + if ((op.insert is not null && oldOp.insert is not null) || (op.delete is not null && oldOp.delete is not null)) { - // Ignore duplicate inserts in the same subscription update. + Log.Warn($"Update with the same primary key was applied multiple times! tableName={update.TableName}"); + // TODO(jdetter): Is this a correctable error? This would be a major error on the + // SpacetimeDB side. continue; } - var obj = table.DecodeValue(bin); - var op = new DbOp + var (insertOp, deleteOp) = op.insert is not null ? (op, oldOp) : (oldOp, op); + op = new DbOp { - table = table, - insert = new(obj, bin), + table = insertOp.table, + delete = deleteOp.delete, + insert = insertOp.insert, }; - - dbOps.Add(op); } + primaryKeyChanges[key] = op; + } + else + { + dbOps.Add(op); } } - break; + } + } + + // Combine primary key updates and non-primary key updates + dbOps.AddRange(primaryKeyChanges.Values); + + return dbOps; + } + + void PreProcessOneOffQuery(OneOffQueryResponse resp) + { + /// This case does NOT produce a list of DBOps, because it should not modify the client cache state! + var messageId = new Guid(resp.MessageId); + if (!waitingOneOffQueries.Remove(messageId, out var resultSource)) + { + Log.Error($"Response to unknown one-off-query: {messageId}"); + return; + } + + resultSource.SetResult(resp); + } + + PreProcessedMessage PreProcessMessage(UnprocessedMessage unprocessed) + { + var dbOps = new List<DbOp>(); + + var message = DecompressDecodeMessage(unprocessed.bytes); + + ReducerEvent<Reducer>? reducerEvent = default; + + // This is all of the inserts + Dictionary<System.Type, HashSet<byte[]>>? subscriptionInserts = null; + + switch (message) + { + case ServerMessage.InitialSubscription(var initSub): + var (ops, subIns) = PreProcessInitialSubscription(initSub); + dbOps = ops; + subscriptionInserts = subIns; + break; case ServerMessage.TransactionUpdate(var transactionUpdate): // Convert the generic event arguments in to a domain specific event object try @@ -428,111 +559,16 @@ HashSet<byte[]> GetInsertHashSet(System.Type tableType, int tableSize) if (transactionUpdate.Status is UpdateStatus.Committed(var committed)) { - primaryKeyChanges = new(); - - // First apply all of the state - foreach (var update in committed.Tables) - { - var tableName = update.TableName; - var table = clientDB.GetTable(tableName); - if (table == null) - { - Log.Error($"Unknown table name: {tableName}"); - continue; - } - - foreach (var cqu in update.Updates) - { - var qu = DecompressDecodeQueryUpdate(cqu); - foreach (var row in BsatnRowListIter(qu.Inserts)) - { - var op = new DbOp { table = table, insert = Decode(table, row, out var pk) }; - if (pk != null) - { - // Compound key that we use for lookup. - // Consists of type of the table (for faster comparison that string names) + actual primary key of the row. - var key = (table.ClientTableType, pk); - - if (primaryKeyChanges.TryGetValue(key, out var oldOp)) - { - if ((op.insert is not null && oldOp.insert is not null) || (op.delete is not null && oldOp.delete is not null)) - { - Log.Warn($"Update with the same primary key was applied multiple times! tableName={tableName}"); - // TODO(jdetter): Is this a correctable error? This would be a major error on the - // SpacetimeDB side. - continue; - } - - var (insertOp, deleteOp) = op.insert is not null ? (op, oldOp) : (oldOp, op); - op = new DbOp - { - table = insertOp.table, - delete = deleteOp.delete, - insert = insertOp.insert, - }; - } - primaryKeyChanges[key] = op; - } - else - { - dbOps.Add(op); - } - } - - foreach (var row in BsatnRowListIter(qu.Deletes)) - { - var op = new DbOp { table = table, delete = Decode(table, row, out var pk) }; - if (pk != null) - { - // Compound key that we use for lookup. - // Consists of type of the table (for faster comparison that string names) + actual primary key of the row. - var key = (table.ClientTableType, pk); - - if (primaryKeyChanges.TryGetValue(key, out var oldOp)) - { - if ((op.insert is not null && oldOp.insert is not null) || (op.delete is not null && oldOp.delete is not null)) - { - Log.Warn($"Update with the same primary key was applied multiple times! tableName={tableName}"); - // TODO(jdetter): Is this a correctable error? This would be a major error on the - // SpacetimeDB side. - continue; - } - - var (insertOp, deleteOp) = op.insert is not null ? (op, oldOp) : (oldOp, op); - op = new DbOp - { - table = insertOp.table, - delete = deleteOp.delete, - insert = insertOp.insert, - }; - } - primaryKeyChanges[key] = op; - } - else - { - dbOps.Add(op); - } - } - } - } - - // Combine primary key updates and non-primary key updates - dbOps.AddRange(primaryKeyChanges.Values); + dbOps = PreProcessDatabaseUpdate(committed); } break; + case ServerMessage.TransactionUpdateLight(var update): + dbOps = PreProcessDatabaseUpdate(update.Update); + break; case ServerMessage.IdentityToken(var identityToken): break; case ServerMessage.OneOffQueryResponse(var resp): - /// This case does NOT produce a list of DBOps, because it should not modify the client cache state! - var messageId = new Guid(resp.MessageId); - - if (!waitingOneOffQueries.Remove(messageId, out var resultSource)) - { - Log.Error($"Response to unknown one-off-query: {messageId}"); - break; - } - - resultSource.SetResult(resp); + PreProcessOneOffQuery(resp); break; default: throw new InvalidOperationException(); @@ -594,7 +630,7 @@ public void Disconnect() /// </summary> /// <param name="uri"> URI of the SpacetimeDB server (ex: https://testnet.spacetimedb.com) /// <param name="addressOrName">The name or address of the database to connect to</param> - internal void Connect(string? token, string uri, string addressOrName, Compression compression) + internal void Connect(string? token, string uri, string addressOrName, Compression compression, bool light) { isClosing = false; @@ -612,7 +648,7 @@ internal void Connect(string? token, string uri, string addressOrName, Compressi { try { - await webSocket.Connect(token, uri, addressOrName, Address, compression); + await webSocket.Connect(token, uri, addressOrName, Address, compression, light); } catch (Exception e) { @@ -736,6 +772,16 @@ private void OnMessageProcessComplete(PreProcessedMessage preProcessed) } break; } + case ServerMessage.TransactionUpdateLight(var update): + { + stats.ParseMessageTracker.InsertRequest(timestamp, $"type={nameof(ServerMessage.TransactionUpdateLight)}"); + + var eventContext = ToEventContext(new Event<Reducer>.UnknownTransaction()); + OnMessageProcessCompleteUpdate(eventContext, dbOps); + + break; + } + case ServerMessage.TransactionUpdate(var transactionUpdate): { var reducer = transactionUpdate.ReducerCall.ReducerName; @@ -811,7 +857,7 @@ internal void OnMessageReceived(byte[] bytes, DateTime timestamp) => _messageQueue.Add(new UnprocessedMessage { bytes = bytes, timestamp = timestamp }); // TODO: this should become [Obsolete] but for now is used by autogenerated code. - public void InternalCallReducer<T>(T args) + public void InternalCallReducer<T>(T args, CallReducerFlags flags) where T : IReducerArgs, new() { if (!webSocket.IsConnected) @@ -820,14 +866,12 @@ public void InternalCallReducer<T>(T args) return; } - webSocket.Send(new ClientMessage.CallReducer( - new CallReducer - { - RequestId = stats.ReducerRequestTracker.StartTrackingRequest(args.ReducerName), - Reducer = args.ReducerName, - Args = IStructuralReadWrite.ToBytes(args) - } - )); + webSocket.Send(new ClientMessage.CallReducer(new CallReducer( + args.ReducerName, + IStructuralReadWrite.ToBytes(args), + stats.ReducerRequestTracker.StartTrackingRequest(args.ReducerName), + (byte)flags + ))); } void IDbConnection.Subscribe(ISubscriptionHandle handle, string[] querySqls) diff --git a/src/WebSocket.cs b/src/WebSocket.cs index 86f47c70..bf65c84e 100644 --- a/src/WebSocket.cs +++ b/src/WebSocket.cs @@ -51,9 +51,14 @@ public WebSocket(ConnectOptions options) public bool IsConnected { get { return Ws != null && Ws.State == WebSocketState.Open; } } - public async Task Connect(string? auth, string host, string nameOrAddress, Address clientAddress, Compression compression) + public async Task Connect(string? auth, string host, string nameOrAddress, Address clientAddress, Compression compression, bool light) { - var url = new Uri($"{host}/database/subscribe/{nameOrAddress}?client_address={clientAddress}&compression={compression}"); + var uri = $"{host}/database/subscribe/{nameOrAddress}?client_address={clientAddress}&compression={compression}"; + if (light) + { + uri += "&light=true"; + } + var url = new Uri(uri); Ws.Options.AddSubProtocol(_options.Protocol); var source = new CancellationTokenSource(10000); diff --git a/tests~/SnapshotTests.VerifyAllTablesParsed.verified.txt b/tests~/SnapshotTests.VerifyAllTablesParsed.verified.txt index ea57bb0e..e3ae0cdc 100644 --- a/tests~/SnapshotTests.VerifyAllTablesParsed.verified.txt +++ b/tests~/SnapshotTests.VerifyAllTablesParsed.verified.txt @@ -8,6 +8,7 @@ OnInsertUser: { eventContext: { Reducers: {Scrubbed}, + SetReducerFlags: {}, Event: {}, Db: {Scrubbed} }, @@ -20,6 +21,7 @@ OnInsertUser: { eventContext: { Reducers: {Scrubbed}, + SetReducerFlags: {}, Event: { ReducerEvent: { Timestamp: DateTimeOffset_1, @@ -40,6 +42,7 @@ OnUpdateUser: { eventContext: { Reducers: {Scrubbed}, + SetReducerFlags: {}, Event: { ReducerEvent: { Timestamp: DateTimeOffset_2, @@ -68,6 +71,7 @@ }, OnSetName: { Reducers: {Scrubbed}, + SetReducerFlags: {}, Event: { ReducerEvent: { Timestamp: DateTimeOffset_2, @@ -87,6 +91,7 @@ OnInsertMessage: { eventContext: { Reducers: {Scrubbed}, + SetReducerFlags: {}, Event: { ReducerEvent: { Timestamp: DateTimeOffset_3, @@ -111,6 +116,7 @@ }, OnSendMessage: { Reducers: {Scrubbed}, + SetReducerFlags: {}, Event: { ReducerEvent: { Timestamp: DateTimeOffset_3, @@ -130,6 +136,7 @@ OnUpdateUser: { eventContext: { Reducers: {Scrubbed}, + SetReducerFlags: {}, Event: { ReducerEvent: { Timestamp: DateTimeOffset_4, @@ -158,6 +165,7 @@ }, OnSetName: { Reducers: {Scrubbed}, + SetReducerFlags: {}, Event: { ReducerEvent: { Timestamp: DateTimeOffset_4, @@ -177,6 +185,7 @@ OnInsertMessage: { eventContext: { Reducers: {Scrubbed}, + SetReducerFlags: {}, Event: { ReducerEvent: { Timestamp: DateTimeOffset_5, @@ -201,6 +210,7 @@ }, OnSendMessage: { Reducers: {Scrubbed}, + SetReducerFlags: {}, Event: { ReducerEvent: { Timestamp: DateTimeOffset_5, @@ -220,6 +230,7 @@ OnInsertMessage: { eventContext: { Reducers: {Scrubbed}, + SetReducerFlags: {}, Event: { ReducerEvent: { Timestamp: DateTimeOffset_6, @@ -244,6 +255,7 @@ }, OnSendMessage: { Reducers: {Scrubbed}, + SetReducerFlags: {}, Event: { ReducerEvent: { Timestamp: DateTimeOffset_6, @@ -263,6 +275,7 @@ OnUpdateUser: { eventContext: { Reducers: {Scrubbed}, + SetReducerFlags: {}, Event: { ReducerEvent: { Timestamp: DateTimeOffset_7, @@ -289,6 +302,7 @@ OnInsertMessage: { eventContext: { Reducers: {Scrubbed}, + SetReducerFlags: {}, Event: { ReducerEvent: { Timestamp: DateTimeOffset_8, @@ -313,6 +327,7 @@ }, OnSendMessage: { Reducers: {Scrubbed}, + SetReducerFlags: {}, Event: { ReducerEvent: { Timestamp: DateTimeOffset_8, @@ -390,4 +405,4 @@ Max: type=InitialSubscription } } -} \ No newline at end of file +} diff --git a/tools~/gen-client-api.sh b/tools~/gen-client-api.sh old mode 100644 new mode 100755 From befe0d1bf46d121276f781aeab850a85db56cfa0 Mon Sep 17 00:00:00 2001 From: Tyler Cloutier <cloutiertyler@aol.com> Date: Tue, 29 Oct 2024 21:47:48 -0400 Subject: [PATCH 40/55] Added missing meta files --- src/CallReducerFlags.cs.meta | 11 +++++++++++ src/Compression.cs.meta | 8 ++++---- .../ClientApi/TransactionUpdateLight.cs.meta | 11 +++++++++++ 3 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 src/CallReducerFlags.cs.meta create mode 100644 src/SpacetimeDB/ClientApi/TransactionUpdateLight.cs.meta diff --git a/src/CallReducerFlags.cs.meta b/src/CallReducerFlags.cs.meta new file mode 100644 index 00000000..dbfee1f8 --- /dev/null +++ b/src/CallReducerFlags.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c0567f0d188b749659b7291c277a2b17 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/Compression.cs.meta b/src/Compression.cs.meta index eef0325c..31f1b190 100644 --- a/src/Compression.cs.meta +++ b/src/Compression.cs.meta @@ -1,11 +1,11 @@ fileFormatVersion: 2 -guid: 03fdc211a87474eeba9f03495eab491e +guid: 78dd2c1276a6e4e859d2c4fad671c2c7 MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/SpacetimeDB/ClientApi/TransactionUpdateLight.cs.meta b/src/SpacetimeDB/ClientApi/TransactionUpdateLight.cs.meta new file mode 100644 index 00000000..50cff86d --- /dev/null +++ b/src/SpacetimeDB/ClientApi/TransactionUpdateLight.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: de607af1bdc1244f894fb952d4199473 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: From c1cfca3a33768f5996095d63fec671d3642986c5 Mon Sep 17 00:00:00 2001 From: Jeremie Pelletier <jeremiep@gmail.com> Date: Thu, 31 Oct 2024 15:42:52 -0400 Subject: [PATCH 41/55] Update quickstart chat server for 0.12 host --- examples~/quickstart/server/.gitignore | 6 +- examples~/quickstart/server/Cargo.lock | 95 +++++++++++++++++++------- examples~/quickstart/server/Cargo.toml | 5 +- examples~/quickstart/server/src/lib.rs | 91 ++++++++++++------------ 4 files changed, 124 insertions(+), 73 deletions(-) diff --git a/examples~/quickstart/server/.gitignore b/examples~/quickstart/server/.gitignore index 91ad2584..31b13f05 100644 --- a/examples~/quickstart/server/.gitignore +++ b/examples~/quickstart/server/.gitignore @@ -3,6 +3,10 @@ debug/ target/ +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + # These are backup files generated by rustfmt **/*.rs.bk @@ -10,4 +14,4 @@ target/ *.pdb # Spacetime ignore -/.spacetime +/.spacetime \ No newline at end of file diff --git a/examples~/quickstart/server/Cargo.lock b/examples~/quickstart/server/Cargo.lock index 372fb071..e0f96b7c 100644 --- a/examples~/quickstart/server/Cargo.lock +++ b/examples~/quickstart/server/Cargo.lock @@ -62,6 +62,18 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bytemuck" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8334215b81e418a0a7bdb8ef0849474f40bb10c8b71f1c4ed315cff49f32494d" + +[[package]] +name = "bytes" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" + [[package]] name = "cfg-if" version = "1.0.0" @@ -144,6 +156,15 @@ dependencies = [ "syn 2.0.32", ] +[[package]] +name = "ethnum" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b90ca2580b73ab6a1f724b76ca11ab632df820fd6040c336200d2c1df7b3c82c" +dependencies = [ + "serde", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -259,6 +280,14 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quickstart-chat-module" +version = "0.1.0" +dependencies = [ + "log", + "spacetimedb", +] + [[package]] name = "quote" version = "1.0.29" @@ -325,6 +354,26 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +[[package]] +name = "serde" +version = "1.0.193" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.193" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.32", +] + [[package]] name = "sha3" version = "0.10.8" @@ -341,21 +390,13 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" -[[package]] -name = "spacetime-module" -version = "0.1.0" -dependencies = [ - "anyhow", - "log", - "spacetimedb", -] - [[package]] name = "spacetimedb" -version = "0.10.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec3fbc884bd532f1f9cddcde961686284091dfa3facd49b71c553df28d30a5f" +checksum = "6b083dadcc676ec1f2bdbd862776ac2fb295c7cd1cd63a671f3e4660c22ce4c7" dependencies = [ + "bytemuck", "derive_more", "getrandom", "log", @@ -369,11 +410,12 @@ dependencies = [ [[package]] name = "spacetimedb-bindings-macro" -version = "0.10.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a5a6d8be2a892ae1d1175b228f12deffb685947cc30d60449dba98219cc3bc2" +checksum = "f2216aa0e94ee5eff29322f7a80b847674a6415bdedca36267d4f9827838ef17" dependencies = [ "bitflags", + "heck", "humantime", "proc-macro2", "quote", @@ -383,29 +425,30 @@ dependencies = [ [[package]] name = "spacetimedb-bindings-sys" -version = "0.10.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a450cdc41a3d6a6a33ba113255c2a1d73dbf32bcf7aa2d272beb58cc6efe9655" +checksum = "bd06c9176160de7138fdd579fcac3fb5735441bdf82896ce9bcadc7000892362" dependencies = [ "spacetimedb-primitives", ] [[package]] name = "spacetimedb-data-structures" -version = "0.10.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d866aa0c287936e13f03206f32c15ce46893076918b7d1d9e1a1421ddca2e6a" +checksum = "0293e7ff788f4d36e85f4029e946145b3ca884d3702b046b1f05497931e2c117" dependencies = [ "hashbrown", "nohash-hasher", + "smallvec", "thiserror", ] [[package]] name = "spacetimedb-lib" -version = "0.10.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0c1d4c3106bdfe395c8f266df1daff9e6259322bd70d05e3f78c7fa67efd14f" +checksum = "e9e92b11f138722de120c6bba2cdc46d0aed348fbeb673bd60d8758a5199e6ac" dependencies = [ "anyhow", "bitflags", @@ -414,6 +457,7 @@ dependencies = [ "hex", "itertools", "spacetimedb-bindings-macro", + "spacetimedb-data-structures", "spacetimedb-primitives", "spacetimedb-sats", "thiserror", @@ -421,33 +465,36 @@ dependencies = [ [[package]] name = "spacetimedb-primitives" -version = "0.10.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f80f43ce78747ae4deb061130fcbedf9c57cbe9d3db8269e2ce403d6cf3a0c" +checksum = "02c6c81cb1df7d6bd77923121d1663f8bbe8e28e669d29d25b335fc81a2a3ccd" dependencies = [ "bitflags", "either", + "itertools", "nohash-hasher", ] [[package]] name = "spacetimedb-sats" -version = "0.10.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ab8da7807277abaef5452d25c3cbc01978297a37d0f00071e52621e655b3a84" +checksum = "b6e07d8e872575b4825eb62c1bbb6f0a50081858664bbe9ee135f977ee7263d2" dependencies = [ "arrayvec", "bitflags", + "bytemuck", + "bytes", "decorum", "derive_more", "enum-as-inner", + "ethnum", "hex", "itertools", "second-stack", "sha3", "smallvec", "spacetimedb-bindings-macro", - "spacetimedb-data-structures", "spacetimedb-primitives", "thiserror", ] diff --git a/examples~/quickstart/server/Cargo.toml b/examples~/quickstart/server/Cargo.toml index 735a5ad6..4775c4e1 100644 --- a/examples~/quickstart/server/Cargo.toml +++ b/examples~/quickstart/server/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "spacetime-module" +name = "quickstart-chat-module" version = "0.1.0" edition = "2021" @@ -9,6 +9,5 @@ edition = "2021" crate-type = ["cdylib"] [dependencies] -spacetimedb = "0.10" +spacetimedb = "0.12.0" log = "0.4" -anyhow = "1.0" diff --git a/examples~/quickstart/server/src/lib.rs b/examples~/quickstart/server/src/lib.rs index 015aaefe..cf986470 100644 --- a/examples~/quickstart/server/src/lib.rs +++ b/examples~/quickstart/server/src/lib.rs @@ -1,5 +1,4 @@ -use spacetimedb::{ReducerContext, Identity, Table, Timestamp}; -use anyhow::{Result, anyhow}; +use spacetimedb::{Identity, ReducerContext, Table, Timestamp}; #[spacetimedb::table(name = user, public)] pub struct User { @@ -16,71 +15,41 @@ pub struct Message { text: String, } -#[spacetimedb::reducer(init)] -pub fn init(_ctx: &ReducerContext) { - -} - -#[spacetimedb::reducer(client_connected)] -pub fn identity_connected(ctx: &ReducerContext) { - if let Some(user) = ctx.db.user().identity().find(&ctx.sender) { - // If this is a returning user, i.e. we already have a `User` with this `Identity`, - // set `online: true`, but leave `name` and `identity` unchanged. - ctx.db.user().identity().update(User { online: true, ..user }); - } else { - // If this is a new user, create a `User` row for the `Identity`, - // which is online, but hasn't set a name. - ctx.db.user().try_insert(User { - name: None, - identity: ctx.sender, - online: true, - }).unwrap(); - } -} - -#[spacetimedb::reducer(client_disconnected)] -pub fn identity_disconnected(ctx: &ReducerContext) { - if let Some(user) = ctx.db.user().identity().find(&ctx.sender) { - ctx.db.user().identity().update(User { online: false, ..user }); - } else { - // This branch should be unreachable, - // as it doesn't make sense for a client to disconnect without connecting first. - log::warn!("Disconnect event for unknown user with identity {:?}", ctx.sender); - } -} - -fn validate_name(name: String) -> Result<String> { +fn validate_name(name: String) -> Result<String, String> { if name.is_empty() { - Err(anyhow!("Names must not be empty")) + Err("Names must not be empty".to_string()) } else { Ok(name) } } #[spacetimedb::reducer] -pub fn set_name(ctx: &ReducerContext, name: String) -> Result<()> { +pub fn set_name(ctx: &ReducerContext, name: String) -> Result<(), String> { let name = validate_name(name)?; - if let Some(user) = ctx.db.user().identity().find(&ctx.sender) { - ctx.db.user().identity().update(User { name: Some(name), ..user }); + if let Some(user) = ctx.db.user().identity().find(ctx.sender) { + ctx.db.user().identity().update(User { + name: Some(name), + ..user + }); Ok(()) } else { - Err(anyhow!("Cannot set name for unknown user")) + Err("Cannot set name for unknown user".to_string()) } } -fn validate_message(text: String) -> Result<String> { +fn validate_message(text: String) -> Result<String, String> { if text.is_empty() { - Err(anyhow!("Messages must not be empty")) + Err("Messages must not be empty".to_string()) } else { Ok(text) } } #[spacetimedb::reducer] -pub fn send_message(ctx: &ReducerContext, text: String) -> Result<()> { +pub fn send_message(ctx: &ReducerContext, text: String) -> Result<(), String> { // Things to consider: // - Rate-limit messages per-user. - // - Reject messages from unnamed users. + // - Reject messages from unnamed user. let text = validate_message(text)?; ctx.db.message().insert(Message { sender: ctx.sender, @@ -89,3 +58,35 @@ pub fn send_message(ctx: &ReducerContext, text: String) -> Result<()> { }); Ok(()) } + +#[spacetimedb::reducer(init)] +// Called when the module is initially published +pub fn init(_ctx: &ReducerContext) {} + +#[spacetimedb::reducer(client_connected)] +pub fn identity_connected(ctx: &ReducerContext) { + if let Some(user) = ctx.db.user().identity().find(ctx.sender) { + // If this is a returning user, i.e. we already have a `User` with this `Identity`, + // set `online: true`, but leave `name` and `identity` unchanged. + ctx.db.user().identity().update(User { online: true, ..user }); + } else { + // If this is a new user, create a `User` row for the `Identity`, + // which is online, but hasn't set a name. + ctx.db.user().insert(User { + name: None, + identity: ctx.sender, + online: true, + }); + } +} + +#[spacetimedb::reducer(client_disconnected)] +pub fn identity_disconnected(ctx: &ReducerContext) { + if let Some(user) = ctx.db.user().identity().find(ctx.sender) { + ctx.db.user().identity().update(User { online: false, ..user }); + } else { + // This branch should be unreachable, + // as it doesn't make sense for a client to disconnect without connecting first. + log::warn!("Disconnect event for unknown user with identity {:?}", ctx.sender); + } +} From fb41046d62ab2aac7d62e8c2d1a865a794edfc13 Mon Sep 17 00:00:00 2001 From: Jeremie Pelletier <jeremiep@gmail.com> Date: Thu, 31 Oct 2024 17:31:39 -0400 Subject: [PATCH 42/55] Update example stdb version --- examples~/quickstart/server/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples~/quickstart/server/Cargo.toml b/examples~/quickstart/server/Cargo.toml index 4775c4e1..db1752dc 100644 --- a/examples~/quickstart/server/Cargo.toml +++ b/examples~/quickstart/server/Cargo.toml @@ -9,5 +9,5 @@ edition = "2021" crate-type = ["cdylib"] [dependencies] -spacetimedb = "0.12.0" +spacetimedb = "1.0.0-rc1" log = "0.4" From 3581ed7484d3684f0d318387c487fd6b9f27fd23 Mon Sep 17 00:00:00 2001 From: John Detter <4099508+jdetter@users.noreply.github.com> Date: Thu, 31 Oct 2024 22:34:26 +0000 Subject: [PATCH 43/55] Fix Reconnection Logic (#168) ## Description of Changes *Describe what has been changed, any new features or bug fixes* - switched our "already connected" logic to using a reference to a `MonoBehaviour` instead of just a bool. `MonoBehaviour`s are typically destroyed when a scene reload happens and in this case we'll want to allow developers to spawn a new `SpacetimeDBNetworkManager` if the previous one has been destroyed. ## API This is *not* an API break. - [ ] This is an API breaking change to the SDK *If the API is breaking, please state below what will break* ## Requires SpacetimeDB PRs *List any PRs here that are required for this SDK change to work* - https://github.com/clockworklabs/SpacetimeDB/pull/1869 ## Testsuite SpacetimeDB branch name: master ## Testing *Write instructions for a test that you performed for this PR* - [x] I have added in several new tests here, one of which is a reconnection test. Also, the reason why we couldn't have more than 1 test before this is that it was required for reconnections to be working in order to have multiple tests running in the testsuite. Now that we have fixed reconnections I have enabled all of the tests. Testsuite passes ![image](https://github.com/user-attachments/assets/09ef5835-f2c7-41f1-af6b-e612ac5e0497) --------- Co-authored-by: John Detter <no-reply@boppygames.gg> Co-authored-by: Mazdak Farrokhzad <twingoow@gmail.com> Co-authored-by: Jeremie Pelletier <jeremiep@gmail.com> Co-authored-by: Zeke Foppa <bfops@users.noreply.github.com> --- .github/workflows/unity-test.yml | 2 +- .../workflows/unity-testsuite-bindings.yml | 4 +- .gitignore | 1 + src/SpacetimeDBClient.cs | 13 +++- src/SpacetimeDBNetworkManager.cs | 76 ++++++++++++------- unity-tests~ | 2 +- 6 files changed, 63 insertions(+), 35 deletions(-) diff --git a/.github/workflows/unity-test.yml b/.github/workflows/unity-test.yml index 774fd847..29e50886 100644 --- a/.github/workflows/unity-test.yml +++ b/.github/workflows/unity-test.yml @@ -7,7 +7,7 @@ on: pull_request: jobs: - test: + unity-testsuite: runs-on: ubuntu-latest steps: - name: Checkout repository diff --git a/.github/workflows/unity-testsuite-bindings.yml b/.github/workflows/unity-testsuite-bindings.yml index 1bfee47d..442abcd1 100644 --- a/.github/workflows/unity-testsuite-bindings.yml +++ b/.github/workflows/unity-testsuite-bindings.yml @@ -7,7 +7,7 @@ on: pull_request: jobs: - test: + check-testsuite-bindings: runs-on: ubuntu-latest steps: - name: Checkout repository @@ -110,7 +110,7 @@ jobs: - name: Check for changes run: | - git diff --exit-code + git diff --exit-code unity-tests~/client/Assets/Scripts/autogen continue-on-error: true - name: Fail if there are changes diff --git a/.gitignore b/.gitignore index 825b31b1..42502a20 100644 --- a/.gitignore +++ b/.gitignore @@ -75,3 +75,4 @@ obj~ # This is used for local paths to SpacetimeDB packages. /nuget.config /nuget.config.meta +.idea/ diff --git a/src/SpacetimeDBClient.cs b/src/SpacetimeDBClient.cs index 7fa8dd6f..d908763c 100644 --- a/src/SpacetimeDBClient.cs +++ b/src/SpacetimeDBClient.cs @@ -36,7 +36,10 @@ public DbConnection Build() } conn.Connect(token, uri, nameOrAddress, compression ?? Compression.Brotli); #if UNITY_5_3_OR_NEWER - SpacetimeDBNetworkManager.ActiveConnections.Add(conn); + if (SpacetimeDBNetworkManager._instance != null) + { + SpacetimeDBNetworkManager._instance.AddConnection(conn); + } #endif return conn; } @@ -163,7 +166,13 @@ protected DbConnectionBase() webSocket.OnMessage += OnMessageReceived; webSocket.OnSendError += a => onSendError?.Invoke(a); #if UNITY_5_3_OR_NEWER - webSocket.OnClose += (e) => SpacetimeDBNetworkManager.ActiveConnections.Remove(this); + webSocket.OnClose += (e) => + { + if (SpacetimeDBNetworkManager._instance != null) + { + SpacetimeDBNetworkManager._instance.RemoveConnection(this); + } + }; #endif networkMessageProcessThread = new Thread(PreProcessMessages); diff --git a/src/SpacetimeDBNetworkManager.cs b/src/SpacetimeDBNetworkManager.cs index ae5e2d4f..379bf63d 100644 --- a/src/SpacetimeDBNetworkManager.cs +++ b/src/SpacetimeDBNetworkManager.cs @@ -6,38 +6,56 @@ namespace SpacetimeDB { - // This class is only used in Unity projects. - // Attach this to a gameobject in your scene to use SpacetimeDB. - public class SpacetimeDBNetworkManager : MonoBehaviour - { - private static bool _alreadyInitialized; + // This class is only used in Unity projects. + // Attach this to a GameObject in your scene to use SpacetimeDB. + public class SpacetimeDBNetworkManager : MonoBehaviour + { + internal static SpacetimeDBNetworkManager? _instance; - public void Awake() - { - // Ensure that users don't create several SpacetimeDBNetworkManager instances. - // We're using a global (static) list of active connections and we don't want several instances to walk over it several times. - if (_alreadyInitialized) - { - throw new InvalidOperationException("SpacetimeDBNetworkManager is a singleton and should only be attached once."); - } - else - { - _alreadyInitialized = true; - } - } + public void Awake() + { + // Ensure that users don't create several SpacetimeDBNetworkManager instances. + // We're using a global (static) list of active connections and we don't want several instances to walk over it several times. + if (_instance != null) + { + throw new InvalidOperationException("SpacetimeDBNetworkManager is a singleton and should only be attached once."); + } + else + { + _instance = this; + } + } - internal static HashSet<IDbConnection> ActiveConnections = new(); + private readonly List<IDbConnection> activeConnections = new(); - private void ForEachConnection(Action<IDbConnection> action) - { - foreach (var conn in ActiveConnections) - { - action(conn); - } - } + public bool AddConnection(IDbConnection conn) + { + if (activeConnections.Contains(conn)) + { + return false; + } + activeConnections.Add(conn); + return true; - private void Update() => ForEachConnection(conn => conn.FrameTick()); - private void OnDestroy() => ForEachConnection(conn => conn.Disconnect()); - } + } + + public bool RemoveConnection(IDbConnection conn) + { + return activeConnections.Remove(conn); + } + + private void ForEachConnection(Action<IDbConnection> action) + { + // It's common to call disconnect from Update, which will then modify the ActiveConnections collection, + // therefore we must reverse-iterate the list of connections. + for (var x = activeConnections.Count - 1; x >= 0; x--) + { + action(activeConnections[x]); + } + } + + private void Update() => ForEachConnection(conn => conn.FrameTick()); + private void OnDestroy() => ForEachConnection(conn => conn.Disconnect()); + } } #endif diff --git a/unity-tests~ b/unity-tests~ index 75047b5e..386db358 160000 --- a/unity-tests~ +++ b/unity-tests~ @@ -1 +1 @@ -Subproject commit 75047b5e58a67ba8e3b652dab17ddde61b661989 +Subproject commit 386db35877caa84a6d8842cde1e8d1a823c7fd78 From 662e3e2a35913e222ade5acb70c4ba0fd76ce5b0 Mon Sep 17 00:00:00 2001 From: Zeke Foppa <bfops@users.noreply.github.com> Date: Fri, 1 Nov 2024 10:07:00 -0700 Subject: [PATCH 44/55] regenerate DLLs from latest SpacetimeDB#release/v1.0.0-rc1 (be63a47e) --- .../dotnet/cs/SpacetimeDB.BSATN.Codegen.dll | Bin 57344 -> 57344 bytes .../SpacetimeDB.BSATN.Runtime.dll | Bin 64512 -> 64512 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/packages/spacetimedb.bsatn.runtime/1.0.0/analyzers/dotnet/cs/SpacetimeDB.BSATN.Codegen.dll b/packages/spacetimedb.bsatn.runtime/1.0.0/analyzers/dotnet/cs/SpacetimeDB.BSATN.Codegen.dll index d25daddb24b51168c1c9b0f330b3047d0d54c689..b4965fc4efc59cb8ac32d6af56fd4518b4091f0f 100644 GIT binary patch delta 238 zcmZoTz}#?vc|r$^63?AU8+(3i7l`o6tvtxl=(T`zq0u9oKOZ+w*-<8`k(6p?oM>X6 zYGiDbYLc9qoMd5XkeHZeZfu-vnPQxnl9pm&W@wUZzS-!~H)fVsCodeGtaB|u;ErlA z!|qr6edfPYl<YZs^6=!IYfBZNf;so0f<V=-58{_^-zHvhZb#Hy?#+SMrCI%x7*ZL` z7>pSb8B7?=fovln+XzUTFeC%XWQHUL3kFLD1E72&LmE(pF%TvLMN+_eQh;J9Kow>T OhF~4$n|I%FWd;Bj<5Y71 delta 238 zcmZoTz}#?vc|r%v<jo<+H}?G4E^zYiGRr%8&%F9J%-box|4aSmDLcv}HH^{>(kv5A zjgpO%63r}(Eld;5lMRdwEG<(^Qj<)~%uSNb(~=U?j5Zrx`o_#sAG&(;WSwgP0&3C* zeKk_;p51@cnhl-5l~3-uwp0Ns_|h9H2vVJXNB-ElHRjQ46=(Ww4!kbS>Tkr5#$W)% zmJEpurVK_5$qdE}NkFz4g9VUf0TfGQFb9en0C@&LRhA4X3?>YzKs6>nHReDv8Azsq O^`tQvZQgyyl^Fo%@=vt@ diff --git a/packages/spacetimedb.bsatn.runtime/1.0.0/lib/netstandard2.1/SpacetimeDB.BSATN.Runtime.dll b/packages/spacetimedb.bsatn.runtime/1.0.0/lib/netstandard2.1/SpacetimeDB.BSATN.Runtime.dll index 96670361095fbd462e5d19ef78c05f770ee2ac24..36caaf3f63b08f275db0631e6bb1319617d76e91 100644 GIT binary patch delta 7578 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 delta 7520 zcmZ`-3tW_C)<4fX?+n8@FwBLEC@@?dE&_szC@S7iLGXg6;tf<ZFGNYEM(hlQhAm$3 z0YAwbhL&#kUdwW8e|TBH*0xsHYTH`-a8p~$e*NmRt<7@3bIv<l*4+=CGw1(5m*+g^ zIq&<<jPD17?+3%qM)k$hf6`uVdMemksMv&ODu5-%vU*`%27sD!kGD|AN-g0sPZ7eD zLdBbh$l&=KfJt{O)EV8)a|3zw!gSNR(`8b+@t9euCAG{`?9Vj1J5!FSj-3j<OzD1l zs`Lx-0E#DuLZX$L!2;9cdmg=md@0%`%WeHdi7Y9I^OlfPx?=F`#UjG)h?Ek=kRGI@ znH59zL8>b(B^-l2)5&kRVpvb9bxRz&%piIshM>P^CV}u2<UO-U#jsMpy>2!tiYJv| z`h|r61N8D@qm^1RQ4ss2WGNmCK&s~+VgdAeXN2?@bLFSe(cayjd1x@K8zA8R>R42h z42owyzBetxq}64_g!6_LpnBJaa9s;;=qb-2RF!Isu~a9ca3{~1bEM3(m{=~TdI_rY zc*j(2HHUtv6dTer3~4EPq=Dl-SfwQ`yicq_&rps$uU4XOT+w-bo5QJWIolpDG`Z1U zCf7I)34O0_XRVl_?Q_-!j)*pR9N3GLBpbGLC!Gd1#F=S;M0}3`u-EKRz+^EQ>bBun zm}*(xG3}IdW22U}+~wb&{G0v$+u5Jx9n&r(Yc^rE)uFVSi4L@G40<S@rzlDKg~@&9 zsg^8!USs6gs4SeLPotB}k(eEeY?JL#Zf^!on#BQk!mv(Joj{rSsPuywiq^XhW(18U zP1?hXXCE%<KaOWGQsMQgkD{s{2iL*@Jp=d@4j@e(4J@8SRd1X@k3NRc{z<MGNo&nQ z??dQ~XF}WI-KNl)Q0etlHaB5x-zH4y+XS+36UNFJDG@Sc8eLG-hymS=u*SH(V=x;x zBI!RhVgg$4YQ$tJ?%x~nD{cg>S^aEJBe)(8`2QMF+8bx6<>=@7G-4{P)u$2UP{;eC zMo{VXRCbCpQvsZ17>GOb=F2-Vqe5|TOnhIlriMC%XBWXdS>g_x7llp+i@Kgp$*J1a zw0?HgA*x4Vw$yNSJ=N?q+Kp+0>_&%DT}X6rKn)JAsip(RFeue!u4yLp3L{g^^H}l2 zxa^zmmA0^+i{TA)NzqQnP8DK?w8sBklu1LvT+|mNe2;o*BI(a2lHQSox^_G1UUI#V z++VBn%tBSWki4dwnUhKk8&9S6sDsnl&zI5TXB^UFd?2ewy`0T{nZtVgULD?Jj2+RV zZp&rAu8}={m3ckJi=#)H<@*H=(|#51_vXo)1(}21A(ig#N-bTfo-e2`Cr}(7gl(*O zfX*-dLZu8YNK&q;@{T8578j)D?7`06HCFqO^3T8LB8>pu$ty8QHCuRo<Vyu%K?lf@ zE~QK8rH!4OVGu2{y6_QYw;~Tk#Yk&Wpoo=WMcJZF78VT-=KKFs{>yKl++38FLMiaF z!rPBpM}jY_BS_ZcJwV~hs+IQSX_224apItu7|U^V;439gK01xEzc%gPvVSuzO&pNX z#r;K`98sL+O2h{6)L2rZy?ZdH!ILaE7MDjRQD?lni3#^y#+~Ta08fhiws@r&B+E+N zB2~^TbZXm6&Iy-0?CB5_{2W4ZQ_xFJ6YLS%y))XCF@Yika-55mD#(CsKxhD?OlXFB z2!Yhl#YzDTfmTLEVzE+zsDnMHI<$h)lOo50Q9cEr3i2RP^rF#_EEKdo7YNV<V_-0& zVIhRZLY|g0=RV;<`*^(eDsc53ilGN>hi1iq>wBC4Wv~OJeD|JgTZ?Fc4ux`3mKM!6 zcbEt_L5FR-#RLt`?Uq<@23%8z!;9uNxnN$-z-+V~wPd61DB4O*C)8MIGd*t^0*@%# z3-b;LaZM{O+bhIjd1JvdD4Q4Fww+f8KoEWm05}ItuJ`2LMakll{OzLuK}$M}pl6fc zqAk9N+!)w^^V-eHC1;Q;hROKY?dB@WiCYNS`1zol%c=M!aw}n7S}%98q7JzS;7%WI z(b915mWW{IOlu8{8%(}iV6S<e^+A}+=(?zKE=IK4k2X3ht?Qvq_r$oh)`wtcPh5?4 z103oTw*ij#iQ5SO&FHN8bm$VpCb-0Cx7R!}Y?E~pe4;}{k6IswtBlUUPFsed7BbVR zw6k#96^|&F(Qfn2@LyUVfk}SUh3FSBpV2vZ-F#m4L4_Y-qY+gzx{hscwbsK%-6p(E zuA|mRDL?+5Qw6+eeGDFFV*~E+l(hkN_8=2%hCMw<s+-{%MrQ+QPg_7^w8ea%dPdm- zzxJc`>f6>Wa8^f{(*%#h`;2gdUfkj1;Kq-<xI;lZd>jfG(GG>N5oY;O7osO%1tWcj zO|Zj{XopR(Uq_<J<uq=ig!p&PDByy1J6vKLZRum{4)~nW7Wg{Ud(FBNzGHYD18-WJ z!Ig<|q8Tj4TUH5qgy6b(-Tafa74Bu!9N}`_Qt=+af9H(-ElSXCSj{$?Cs)va!NdMI znxHl~;72q;Pr_+NJP8iNUU-k8gk<}`puJ#_qH;Qp>LJ)Vg!1bb%96@SQMc5m^%Zef zl(rXPT6wDSjJPE?ugDR%w6|6~uOL{n3W4lk`N^sZ)JLy=+<RTzR%hBGMa(G5c3u3! zQf3<<(){S1K!YhtEMlbZGg>V1+it7NZP8+7Ph6EPMm*RjE>;}r6Bj3*VWeMSapE;b zXT5v@#Ea9q(fk|3JZro-??-PNHrV3D`;7GYPY~`rDpa5U1hJYCO^45xC?4{oE<}UG z6O8ofNEXNZh^8Z1oM3bvc;Zt;rw;MVpRlEh8-7=s|1|Lpqk25aX4_z4!pA7x8NLqP zV@nq<9ieTlHABQR(y!@Ek)hknKN)JQnIgxJENZ(gQ{*%9Py7(kqzCdfJw$xSi1u{M zmL>k`M_q`9iog2<>6#ucLh&P~e<l#cGoowys5M8V=?J%^*+z&teoxv`u2{r~wlx2w zZKPPokoI)SHcI&Xfw$G)+496qKl+8`kG9cb9)7B$DWSO-D_&rv&qcm?$#1)@{>7Fr zUYGr=5^=`HRwaw`a#d9+4sntAM?S^tL;Ow^-K*z`DfC$KbNlZju2Sro(92a<6Sq$M zZDKDs?tbFDVrD@v=X`*;Ch-=2V(iX$jkz|FJhhiQv5x$n7OjQ7+}0Z6G$HVNb9d~5 z2Z`$xRmHvB;Pu44EB;*4%l(tNkHw@Jz1+oz$nSIUGJcco&eySlxLYC$zsh!V>o*c7 zl&$#HwVRu~iMU|pyE(mF|A&bipj6E5<-TOjqr8jXW4mMDswKZ+N-lnV?dEnoLfm-e z$bw$3qOQ(%OP!|dSV|n9p!}+?gP(sUn58(E6aN@~7CC2}rHo@lSFQ<4m8pGfrOF0I z`pnN(9%Dp<WrEpC6C?e?ovnPNd-9OaR<1CjAs_FUqkN^K`Q|Q!a}|M~D5-p!^7%@j zAJLUNUkPVKQ$Ed6rnq$*PWckY0wu%mDPV<Tp_0Rh?(!QQ@?P(8?7~W=a+RJMlgU&c zb>>0k;_6=RXI~xOGHdHUP~@G4Xj$0sf;Mn-gkjg#OgXgi_gd!G*(UQ^6(0$(R+YXT z)8t1xZrKk*92|ytD1-#K55H5dlb3ha*vCKte&m`AB`^hM!Bn_MRy0@HDq%6Kf~Bw; zR=^rqEiX6M7;{Ci^RPB&SFZ3Ti)xpKH>i`g6DT*i-c*M$GekV<I)jfjdMGw=V@}a$ zCS;mi6OajZUP7JvepR85W<oh;zM)=Kf}Ij2MFlFq$1}gG(DV2TG@5UtPLI*63Oypx z2cO!v>`GbK9K$W;b@OtRO|JJ1GP-485Y8>HbBorIlUr^CpI7_ivqFGfd?hqh8`D}Y zhFnuu;d&;>Go-r?nDPv5j`x-0T+?x``#9HqoHG%BN&Ca@-x&-J$dr4ZoNI`L;o5gk zjuIhp5YsPSMH=O55fUo%_a#J6V09AH%hV2N4_~H|<1%LCBl{xcllvl-y&zxP_k%pX ze+quR_<sLN&R%g~CS{ke9=Imk4@St}983_W<mH1=rYiw8fD8P1@Vx?Bufx0-;Sn?G z$56(gB!^CxXP7_3KEGwXi}}yYaS$5%88b5YP^5*RA&`=2S%=OGr3Pe3N1Tg_?y!=v z!^*2!R|`3`<FM6dqn)(dxR#$fBD@J9<eLx@31cJjLx`?GeOkZe%r8ehv7e9o)XuV# zWfyy138CEh6AeZQJL#&O^0=9aWhQ}{d}hWmGm)8cW|n&y_!t~w^I>K>?bM!A%$!9g zvELQeud<%+AZxvY7H(&Gh~+5<6>$}1Xec<z>Sh`1BtG9s4asMwob__2mz;b|wKLVu zdMC>+X1Z7h7pG%65&L^0e4;B73L|{XG%(YEOj$%bGlyMtrk$)qD95tQXIakD7h01` zI|!w^+L`KP*~JpVIE-aN7^TY(BhQJf%U6zU_9oKdH6&7h4kz-0iFCf5i4@tz5|TJe z67g=<^I6Yly_}_wWjo7GmR&4i5XZ7i7{r4zh}OzyX5t_(Em_W>eGm^8>zypSSb~Rm z@Ngp5-K^)cENAIs+0L?)Wfx0G<|vl=EYBuW=g%fnpRXo6v|-O|GUvIA$St4c#3HJ4 zViApDIWs<%4a_$npV+URnZrd?y!OS56UFrMSyW{CEXvc)dOPc#taq~B#S%)nB$oLs z%USwZwzKS%buV8NiE`O1e+rz0_j`e_0y#k&mY8HrF_syZ7_S)rVYHhvOw&!JrUj-K zOc?>A0wxC(2P_O&6X28kwdwYF=#O{!NbulKSd$?NrpTYQxuQ~=b9}2Zx)riP!N<Tb zlt&;37kC<_0w*CCWx5!JGDD0;J`<$^qs3U1W5hUQ#%cpjY!X7s*4L{B?1H`UD-1gg z=i!g=3496v3%9`_tRg~2z2P-pR)0{V<)JtF8%hI~%D3K_85D=r#N$&Y9-lJta=^)z z*^^*7K7A??ufV6$O8nXFUPP<#wHjY*@U<2t{q~y)7W}c+g?oAR<R3Dgg|F3%Fx&Vn z6sT*(*XpxiHD%-bD0~&;t3?b8Sc|U~F%I9yY1*$XQSy_IrpUKGijE2_!JikZFgN|3 zFuySJxu&=Yk9x<qe?DuiMdtr`SA{v$;X5kqzJ_k4(dZT;D%ck4C>No2-yy~BYqk5@ z0zw^?!r^Pu-|GkZ-`#o`8u|t_*nN$DhZY(8aYSfKKMKdCb_yq;#dl0Z8MX3{cN@|y zstH#xxv_>;{3!$9+AIb)m{qq>1_oj+REQq5Q}+8bDTPw_Gg;85PtqG`F$4&6K!9qF zGMbIrx=&SQiG_A&3=od`CL`&MtnWmLD^mG4NMYF44clA+SQTvpcjl;Xanv7n)E`CC z;oFAJSR<1I9rbSQgRY^XfZIojXeA8mPSj`)P|($0-)_ccJ||EK4p9B6IVo535>|$8 z+VsC}7m+P0F4InJ`H95aB-&8aKPSKctR%95aATmUJ0DqGBqFpWSF?l{Tge-816E4F z;oGXeZPQI{Ml!XTfzFe9;8)rLK~ZSG_@5j5F9U$FWVE}n`F9(5Fx(<06A!=<NFIF9 zcszXO^2cj#ZT$6FiWfJI)dac~jKD@VnCaND$Gub|QkVzJ;%e%##RGY1e#0gW48W`* zRJPwyFT382XQ4aN8#=oEj&113yV@G)Ul@@#@WxuD7|*rkuC3CU>uFm4b*p{@9TVlJ zlcqM(7P#I9`N5Yl;SIVCFCw({?4?omyBXsmNDKDt7J(ddGbNza=%E9V)i+}T8dz`C zT5rY*Pu7ae6}c6|vX*5ptr$Kcd&IDcoMoBWnYp>khgL2fIy`6SvYZu5D^_G_pMLeR zLCvZD{;YiSn;9W;@bj-K%}s>|fB5h(&qsdoqa66{5x1X=jPaA`J#pd9pPc_ZHs*5b zV?PFISH3MZPS3)-QzpK0@xCz(vS1lx<0s<^7!D&)8-ZK}-a?n5Pd2`C@!xV7iuaeL XD2JnEDB5x`o?=!&miE{Wb%y@~hqb_k From faacb45ab0c1c4d1c70f97fd9e3d788377a7b2b6 Mon Sep 17 00:00:00 2001 From: Zeke Foppa <bfops@users.noreply.github.com> Date: Fri, 1 Nov 2024 10:09:45 -0700 Subject: [PATCH 45/55] empty commit to bump CI From be120394edfe6363b08f82f079e55e9abc0807c5 Mon Sep 17 00:00:00 2001 From: Jeremie Pelletier <jeremiep@gmail.com> Date: Mon, 4 Nov 2024 14:17:05 -0500 Subject: [PATCH 46/55] Update the quickstart module (#185) Fix the quickstart client program, also removes the rust server program (it lives in the stdb main repo under modules/quickstart-chat) Second subscription resets the first one without the fix SpacetimeDB branch name: master --- .../quickstart/server/.cargo/config.toml | 2 - examples~/quickstart/server/.gitignore | 17 - examples~/quickstart/server/Cargo.lock | 586 ------------------ examples~/quickstart/server/Cargo.toml | 13 - examples~/quickstart/server/src/lib.rs | 92 --- 5 files changed, 710 deletions(-) delete mode 100644 examples~/quickstart/server/.cargo/config.toml delete mode 100644 examples~/quickstart/server/.gitignore delete mode 100644 examples~/quickstart/server/Cargo.lock delete mode 100644 examples~/quickstart/server/Cargo.toml delete mode 100644 examples~/quickstart/server/src/lib.rs diff --git a/examples~/quickstart/server/.cargo/config.toml b/examples~/quickstart/server/.cargo/config.toml deleted file mode 100644 index f4e8c002..00000000 --- a/examples~/quickstart/server/.cargo/config.toml +++ /dev/null @@ -1,2 +0,0 @@ -[build] -target = "wasm32-unknown-unknown" diff --git a/examples~/quickstart/server/.gitignore b/examples~/quickstart/server/.gitignore deleted file mode 100644 index 31b13f05..00000000 --- a/examples~/quickstart/server/.gitignore +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Cargo -# will have compiled files and executables -debug/ -target/ - -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -Cargo.lock - -# These are backup files generated by rustfmt -**/*.rs.bk - -# MSVC Windows builds of rustc generate these, which store debugging information -*.pdb - -# Spacetime ignore -/.spacetime \ No newline at end of file diff --git a/examples~/quickstart/server/Cargo.lock b/examples~/quickstart/server/Cargo.lock deleted file mode 100644 index e0f96b7c..00000000 --- a/examples~/quickstart/server/Cargo.lock +++ /dev/null @@ -1,586 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "ahash" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", -] - -[[package]] -name = "allocator-api2" -version = "0.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" - -[[package]] -name = "anyhow" -version = "1.0.71" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" - -[[package]] -name = "approx" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0e60b75072ecd4168020818c0107f2857bb6c4e64252d8d3983f6263b40a5c3" -dependencies = [ - "num-traits", -] - -[[package]] -name = "arrayvec" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" - -[[package]] -name = "autocfg" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" - -[[package]] -name = "bitflags" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "bytemuck" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8334215b81e418a0a7bdb8ef0849474f40bb10c8b71f1c4ed315cff49f32494d" - -[[package]] -name = "bytes" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "convert_case" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" - -[[package]] -name = "cpufeatures" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" -dependencies = [ - "libc", -] - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "decorum" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "281759d3c8a14f5c3f0c49363be56810fcd7f910422f97f2db850c2920fde5cf" -dependencies = [ - "approx", - "num-traits", -] - -[[package]] -name = "derive_more" -version = "0.99.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version", - "syn 1.0.109", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - -[[package]] -name = "either" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" - -[[package]] -name = "enum-as-inner" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ffccbb6966c05b32ef8fbac435df276c4ae4d3dc55a8cd0eb9745e6c12f546a" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.32", -] - -[[package]] -name = "ethnum" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b90ca2580b73ab6a1f724b76ca11ab632df820fd6040c336200d2c1df7b3c82c" -dependencies = [ - "serde", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash", - "allocator-api2", -] - -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" - -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - -[[package]] -name = "keccak" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f6d5ed8676d904364de097082f4e7d240b571b67989ced0240f08b7f966f940" -dependencies = [ - "cpufeatures", -] - -[[package]] -name = "libc" -version = "0.2.155" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" - -[[package]] -name = "log" -version = "0.4.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" - -[[package]] -name = "nohash-hasher" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" - -[[package]] -name = "num-traits" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" -dependencies = [ - "autocfg", -] - -[[package]] -name = "once_cell" -version = "1.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" - -[[package]] -name = "ppv-lite86" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" - -[[package]] -name = "proc-macro2" -version = "1.0.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78803b62cbf1f46fde80d7c0e803111524b9877184cfe7c3033659490ac7a7da" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quickstart-chat-module" -version = "0.1.0" -dependencies = [ - "log", - "spacetimedb", -] - -[[package]] -name = "quote" -version = "1.0.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom", -] - -[[package]] -name = "rustc_version" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" -dependencies = [ - "semver", -] - -[[package]] -name = "scoped-tls" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" - -[[package]] -name = "second-stack" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4904c83c6e51f1b9b08bfa5a86f35a51798e8307186e6f5513852210a219c0bb" - -[[package]] -name = "semver" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" - -[[package]] -name = "serde" -version = "1.0.193" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.193" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.32", -] - -[[package]] -name = "sha3" -version = "0.10.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" -dependencies = [ - "digest", - "keccak", -] - -[[package]] -name = "smallvec" -version = "1.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" - -[[package]] -name = "spacetimedb" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b083dadcc676ec1f2bdbd862776ac2fb295c7cd1cd63a671f3e4660c22ce4c7" -dependencies = [ - "bytemuck", - "derive_more", - "getrandom", - "log", - "rand", - "scoped-tls", - "spacetimedb-bindings-macro", - "spacetimedb-bindings-sys", - "spacetimedb-lib", - "spacetimedb-primitives", -] - -[[package]] -name = "spacetimedb-bindings-macro" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2216aa0e94ee5eff29322f7a80b847674a6415bdedca36267d4f9827838ef17" -dependencies = [ - "bitflags", - "heck", - "humantime", - "proc-macro2", - "quote", - "spacetimedb-primitives", - "syn 2.0.32", -] - -[[package]] -name = "spacetimedb-bindings-sys" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd06c9176160de7138fdd579fcac3fb5735441bdf82896ce9bcadc7000892362" -dependencies = [ - "spacetimedb-primitives", -] - -[[package]] -name = "spacetimedb-data-structures" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0293e7ff788f4d36e85f4029e946145b3ca884d3702b046b1f05497931e2c117" -dependencies = [ - "hashbrown", - "nohash-hasher", - "smallvec", - "thiserror", -] - -[[package]] -name = "spacetimedb-lib" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9e92b11f138722de120c6bba2cdc46d0aed348fbeb673bd60d8758a5199e6ac" -dependencies = [ - "anyhow", - "bitflags", - "derive_more", - "enum-as-inner", - "hex", - "itertools", - "spacetimedb-bindings-macro", - "spacetimedb-data-structures", - "spacetimedb-primitives", - "spacetimedb-sats", - "thiserror", -] - -[[package]] -name = "spacetimedb-primitives" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02c6c81cb1df7d6bd77923121d1663f8bbe8e28e669d29d25b335fc81a2a3ccd" -dependencies = [ - "bitflags", - "either", - "itertools", - "nohash-hasher", -] - -[[package]] -name = "spacetimedb-sats" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6e07d8e872575b4825eb62c1bbb6f0a50081858664bbe9ee135f977ee7263d2" -dependencies = [ - "arrayvec", - "bitflags", - "bytemuck", - "bytes", - "decorum", - "derive_more", - "enum-as-inner", - "ethnum", - "hex", - "itertools", - "second-stack", - "sha3", - "smallvec", - "spacetimedb-bindings-macro", - "spacetimedb-primitives", - "thiserror", -] - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "239814284fd6f1a4ffe4ca893952cdd93c224b6a1571c9a9eadd670295c0c9e2" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "thiserror" -version = "1.0.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a35fc5b8971143ca348fa6df4f024d4d55264f3468c71ad1c2f365b0a4d58c42" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.32", -] - -[[package]] -name = "typenum" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" - -[[package]] -name = "unicode-ident" -version = "1.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22049a19f4a68748a168c0fc439f9516686aa045927ff767eca0a85101fb6e73" - -[[package]] -name = "version_check" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "zerocopy" -version = "0.7.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.7.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.32", -] diff --git a/examples~/quickstart/server/Cargo.toml b/examples~/quickstart/server/Cargo.toml deleted file mode 100644 index db1752dc..00000000 --- a/examples~/quickstart/server/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "quickstart-chat-module" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[lib] -crate-type = ["cdylib"] - -[dependencies] -spacetimedb = "1.0.0-rc1" -log = "0.4" diff --git a/examples~/quickstart/server/src/lib.rs b/examples~/quickstart/server/src/lib.rs deleted file mode 100644 index cf986470..00000000 --- a/examples~/quickstart/server/src/lib.rs +++ /dev/null @@ -1,92 +0,0 @@ -use spacetimedb::{Identity, ReducerContext, Table, Timestamp}; - -#[spacetimedb::table(name = user, public)] -pub struct User { - #[primary_key] - identity: Identity, - name: Option<String>, - online: bool, -} - -#[spacetimedb::table(name = message, public)] -pub struct Message { - sender: Identity, - sent: Timestamp, - text: String, -} - -fn validate_name(name: String) -> Result<String, String> { - if name.is_empty() { - Err("Names must not be empty".to_string()) - } else { - Ok(name) - } -} - -#[spacetimedb::reducer] -pub fn set_name(ctx: &ReducerContext, name: String) -> Result<(), String> { - let name = validate_name(name)?; - if let Some(user) = ctx.db.user().identity().find(ctx.sender) { - ctx.db.user().identity().update(User { - name: Some(name), - ..user - }); - Ok(()) - } else { - Err("Cannot set name for unknown user".to_string()) - } -} - -fn validate_message(text: String) -> Result<String, String> { - if text.is_empty() { - Err("Messages must not be empty".to_string()) - } else { - Ok(text) - } -} - -#[spacetimedb::reducer] -pub fn send_message(ctx: &ReducerContext, text: String) -> Result<(), String> { - // Things to consider: - // - Rate-limit messages per-user. - // - Reject messages from unnamed user. - let text = validate_message(text)?; - ctx.db.message().insert(Message { - sender: ctx.sender, - text, - sent: ctx.timestamp, - }); - Ok(()) -} - -#[spacetimedb::reducer(init)] -// Called when the module is initially published -pub fn init(_ctx: &ReducerContext) {} - -#[spacetimedb::reducer(client_connected)] -pub fn identity_connected(ctx: &ReducerContext) { - if let Some(user) = ctx.db.user().identity().find(ctx.sender) { - // If this is a returning user, i.e. we already have a `User` with this `Identity`, - // set `online: true`, but leave `name` and `identity` unchanged. - ctx.db.user().identity().update(User { online: true, ..user }); - } else { - // If this is a new user, create a `User` row for the `Identity`, - // which is online, but hasn't set a name. - ctx.db.user().insert(User { - name: None, - identity: ctx.sender, - online: true, - }); - } -} - -#[spacetimedb::reducer(client_disconnected)] -pub fn identity_disconnected(ctx: &ReducerContext) { - if let Some(user) = ctx.db.user().identity().find(ctx.sender) { - ctx.db.user().identity().update(User { online: false, ..user }); - } else { - // This branch should be unreachable, - // as it doesn't make sense for a client to disconnect without connecting first. - log::warn!("Disconnect event for unknown user with identity {:?}", ctx.sender); - } -} From 04d084ea2f6f83d53550067093dc524a013aca60 Mon Sep 17 00:00:00 2001 From: Zeke Foppa <196249+bfops@users.noreply.github.com> Date: Wed, 6 Nov 2024 11:29:48 -0800 Subject: [PATCH 47/55] Bump Unity SDK version to RC1 (#187) ## Description of Changes Just bump `package.json` to `1.0.0-rc1` instead of `1.0.0`. ## API No breaking changes ## Requires SpacetimeDB PRs None ## Testing Co-authored-by: Zeke Foppa <bfops@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 015b4e0a..7a1c5a6e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "com.clockworklabs.spacetimedbsdk", "displayName": "SpacetimeDB SDK", - "version": "1.0.0", + "version": "1.0.0-rc1", "description": "The SpacetimeDB Client SDK is a software development kit (SDK) designed to interact with and manipulate SpacetimeDB modules..", "keywords": [], "author": { From 87071668b9cec45a59d2d3caf7e65b9abe29cef0 Mon Sep 17 00:00:00 2001 From: Zeke Foppa <bfops@users.noreply.github.com> Date: Wed, 6 Nov 2024 13:09:40 -0800 Subject: [PATCH 48/55] update --- .../{1.0.0.meta => 1.0.0-rc1.meta} | 0 .../{1.0.0 => 1.0.0-rc1}/analyzers.meta | 0 .../analyzers/dotnet.meta | 0 .../analyzers/dotnet/cs.meta | 0 .../dotnet/cs/SpacetimeDB.BSATN.Codegen.dll | Bin 57344 -> 57344 bytes .../cs/SpacetimeDB.BSATN.Codegen.dll.meta | 0 .../{1.0.0 => 1.0.0-rc1}/lib.meta | 0 .../lib/netstandard2.1.meta | 0 .../SpacetimeDB.BSATN.Runtime.dll | Bin 64512 -> 64512 bytes .../SpacetimeDB.BSATN.Runtime.dll.meta | 0 10 files changed, 0 insertions(+), 0 deletions(-) rename packages/spacetimedb.bsatn.runtime/{1.0.0.meta => 1.0.0-rc1.meta} (100%) rename packages/spacetimedb.bsatn.runtime/{1.0.0 => 1.0.0-rc1}/analyzers.meta (100%) rename packages/spacetimedb.bsatn.runtime/{1.0.0 => 1.0.0-rc1}/analyzers/dotnet.meta (100%) rename packages/spacetimedb.bsatn.runtime/{1.0.0 => 1.0.0-rc1}/analyzers/dotnet/cs.meta (100%) rename packages/spacetimedb.bsatn.runtime/{1.0.0 => 1.0.0-rc1}/analyzers/dotnet/cs/SpacetimeDB.BSATN.Codegen.dll (98%) mode change 100644 => 100755 rename packages/spacetimedb.bsatn.runtime/{1.0.0 => 1.0.0-rc1}/analyzers/dotnet/cs/SpacetimeDB.BSATN.Codegen.dll.meta (100%) rename packages/spacetimedb.bsatn.runtime/{1.0.0 => 1.0.0-rc1}/lib.meta (100%) rename packages/spacetimedb.bsatn.runtime/{1.0.0 => 1.0.0-rc1}/lib/netstandard2.1.meta (100%) rename packages/spacetimedb.bsatn.runtime/{1.0.0 => 1.0.0-rc1}/lib/netstandard2.1/SpacetimeDB.BSATN.Runtime.dll (99%) mode change 100644 => 100755 rename packages/spacetimedb.bsatn.runtime/{1.0.0 => 1.0.0-rc1}/lib/netstandard2.1/SpacetimeDB.BSATN.Runtime.dll.meta (100%) diff --git a/packages/spacetimedb.bsatn.runtime/1.0.0.meta b/packages/spacetimedb.bsatn.runtime/1.0.0-rc1.meta similarity index 100% rename from packages/spacetimedb.bsatn.runtime/1.0.0.meta rename to packages/spacetimedb.bsatn.runtime/1.0.0-rc1.meta diff --git a/packages/spacetimedb.bsatn.runtime/1.0.0/analyzers.meta b/packages/spacetimedb.bsatn.runtime/1.0.0-rc1/analyzers.meta similarity index 100% rename from packages/spacetimedb.bsatn.runtime/1.0.0/analyzers.meta rename to packages/spacetimedb.bsatn.runtime/1.0.0-rc1/analyzers.meta diff --git a/packages/spacetimedb.bsatn.runtime/1.0.0/analyzers/dotnet.meta b/packages/spacetimedb.bsatn.runtime/1.0.0-rc1/analyzers/dotnet.meta similarity index 100% rename from packages/spacetimedb.bsatn.runtime/1.0.0/analyzers/dotnet.meta rename to packages/spacetimedb.bsatn.runtime/1.0.0-rc1/analyzers/dotnet.meta diff --git a/packages/spacetimedb.bsatn.runtime/1.0.0/analyzers/dotnet/cs.meta b/packages/spacetimedb.bsatn.runtime/1.0.0-rc1/analyzers/dotnet/cs.meta similarity index 100% rename from packages/spacetimedb.bsatn.runtime/1.0.0/analyzers/dotnet/cs.meta rename to packages/spacetimedb.bsatn.runtime/1.0.0-rc1/analyzers/dotnet/cs.meta diff --git a/packages/spacetimedb.bsatn.runtime/1.0.0/analyzers/dotnet/cs/SpacetimeDB.BSATN.Codegen.dll b/packages/spacetimedb.bsatn.runtime/1.0.0-rc1/analyzers/dotnet/cs/SpacetimeDB.BSATN.Codegen.dll old mode 100644 new mode 100755 similarity index 98% rename from packages/spacetimedb.bsatn.runtime/1.0.0/analyzers/dotnet/cs/SpacetimeDB.BSATN.Codegen.dll rename to packages/spacetimedb.bsatn.runtime/1.0.0-rc1/analyzers/dotnet/cs/SpacetimeDB.BSATN.Codegen.dll index b4965fc4efc59cb8ac32d6af56fd4518b4091f0f..5a2ceb283098edb4405a918b43bb8b7b3dcd675a GIT binary patch delta 238 zcmZoTz}#?vc|r%vx^0SwH}?G4F7RAxC;N+*MQ)9cQ)^#ErA*p9Wk;E$hLO3sfkA4T zWr~S~nPHNNxq*>!nz3n;xv80frMa=Oahi!qidkYx(q^Me-<VlyCc3vw*0~lSQ1w72 z>)nP2Zkw)_COa)rd^5S{+EN9Wpb=CMq}u4|mHRFa4$e0A_`7^_;B{$Me<KESAT$8N zRE9JLONJB%69x+gGX_J3Bp?eUW5i$#lrd&71&WvhNi(3BC6HwdhG{@mCO}<g42eK8 KY4h$ouFL=}E>UCv delta 238 zcmZoTz}#?vc|r$^63?AU8+(3i7l`o6tvtxl=(T`zq0u9oKOZ+w*-<8`k(6p?oM>X6 zYGiDbYLc9qoMd5XkeHZeZfu-vnPQxnl9pm&W@wUZzS-!~H)fVsCodeGtaB|u;ErlA z!|qr6edfPYl<YZs^6=!IYfBZNf;so0f<V=-58{_^-zHvhZb#Hy?#+SMrCI%x7*ZL` z7>pSb8B7?=fovln+XzUTFeC%XWQHUL3kFLD1E72&LmE(pF%TvLMN+_eQh;J9Kow>T OhF~4$n|I%FWd;Bj<5Y71 diff --git a/packages/spacetimedb.bsatn.runtime/1.0.0/analyzers/dotnet/cs/SpacetimeDB.BSATN.Codegen.dll.meta b/packages/spacetimedb.bsatn.runtime/1.0.0-rc1/analyzers/dotnet/cs/SpacetimeDB.BSATN.Codegen.dll.meta similarity index 100% rename from packages/spacetimedb.bsatn.runtime/1.0.0/analyzers/dotnet/cs/SpacetimeDB.BSATN.Codegen.dll.meta rename to packages/spacetimedb.bsatn.runtime/1.0.0-rc1/analyzers/dotnet/cs/SpacetimeDB.BSATN.Codegen.dll.meta diff --git a/packages/spacetimedb.bsatn.runtime/1.0.0/lib.meta b/packages/spacetimedb.bsatn.runtime/1.0.0-rc1/lib.meta similarity index 100% rename from packages/spacetimedb.bsatn.runtime/1.0.0/lib.meta rename to packages/spacetimedb.bsatn.runtime/1.0.0-rc1/lib.meta diff --git a/packages/spacetimedb.bsatn.runtime/1.0.0/lib/netstandard2.1.meta b/packages/spacetimedb.bsatn.runtime/1.0.0-rc1/lib/netstandard2.1.meta similarity index 100% rename from packages/spacetimedb.bsatn.runtime/1.0.0/lib/netstandard2.1.meta rename to packages/spacetimedb.bsatn.runtime/1.0.0-rc1/lib/netstandard2.1.meta diff --git a/packages/spacetimedb.bsatn.runtime/1.0.0/lib/netstandard2.1/SpacetimeDB.BSATN.Runtime.dll b/packages/spacetimedb.bsatn.runtime/1.0.0-rc1/lib/netstandard2.1/SpacetimeDB.BSATN.Runtime.dll old mode 100644 new mode 100755 similarity index 99% rename from packages/spacetimedb.bsatn.runtime/1.0.0/lib/netstandard2.1/SpacetimeDB.BSATN.Runtime.dll rename to packages/spacetimedb.bsatn.runtime/1.0.0-rc1/lib/netstandard2.1/SpacetimeDB.BSATN.Runtime.dll index 36caaf3f63b08f275db0631e6bb1319617d76e91..c38d14a14d0b466f824e8f369693e0166125d1f8 GIT binary patch delta 238 zcmZqp!QAkJc|r%vKAk5^H})*KA;7Vt=XCB;b?2o|HGWQC^T>L$!_6`g4I^`N1B28w z%M=p}Gs7eka|0veG-J~wb5k<|OLJpm<1`bK6tl#Xq|Nu<-eP9ibhYKi<UJn)1ZMMG zO+FdB-G4#sk7phhHD!}!J}p&%2`WGZfvO`81uCqQU3%T#=!2=?=7*nxS^bR|%z@AV z2vZr-7%Ul57)%%}7|a+98Ipi3kc<(7F;K>s!4xQB4kXQhVwOObF&L%+Rha;FnK2{+ L$)wHAKWdo)v(8bl delta 238 zcmZqp!QAkJc|r$^OX-5e8+(@A5Gb~C5_xA-;yRP7`ph+Pj`+<EH_Jpcl2Xl#6HUxh zjf{;_O_EcSlPoL^5);$Rjg6BnQ;ZW+(o!tU3{8^FH{W}Ei<u=O^7*I9dp-sT%<Ge~ z%2izNv0ME3nHJWr=*cplmMTC6Gk!w_fvSso+Mno_=5G7-dAIuZ%@02Xv-&47q%xQ> z7&9a?m@t?F*+xLN5s)@vNCuM03`q<Y43-QAK>0+5G@uG&AWQ~|q=5CL0L4;(D$E!R N!8*)0H~*+*1^@%zQ5*mO diff --git a/packages/spacetimedb.bsatn.runtime/1.0.0/lib/netstandard2.1/SpacetimeDB.BSATN.Runtime.dll.meta b/packages/spacetimedb.bsatn.runtime/1.0.0-rc1/lib/netstandard2.1/SpacetimeDB.BSATN.Runtime.dll.meta similarity index 100% rename from packages/spacetimedb.bsatn.runtime/1.0.0/lib/netstandard2.1/SpacetimeDB.BSATN.Runtime.dll.meta rename to packages/spacetimedb.bsatn.runtime/1.0.0-rc1/lib/netstandard2.1/SpacetimeDB.BSATN.Runtime.dll.meta From 962db343964da9142dd094085c59afee1e334f79 Mon Sep 17 00:00:00 2001 From: Zeke Foppa <bfops@users.noreply.github.com> Date: Wed, 6 Nov 2024 14:39:50 -0800 Subject: [PATCH 49/55] empty commit to bump CI From 24c599778e030485d151f9033f5103ddb1897913 Mon Sep 17 00:00:00 2001 From: Zeke Foppa <bfops@users.noreply.github.com> Date: Wed, 6 Nov 2024 14:45:39 -0800 Subject: [PATCH 50/55] fix unity sdk tests? --- .github/workflows/unity-test.yml | 2 +- .github/workflows/unity-testsuite-bindings.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/unity-test.yml b/.github/workflows/unity-test.yml index 29e50886..c0623c9b 100644 --- a/.github/workflows/unity-test.yml +++ b/.github/workflows/unity-test.yml @@ -16,7 +16,7 @@ jobs: - name: Checkout submodule run: | git submodule init - git submodule update + git submodule update --recursive cd unity-tests~ git checkout jdetter/circle-game-testsuite # Grab the branch name from the PR description. If it's not found, master will be used instead. diff --git a/.github/workflows/unity-testsuite-bindings.yml b/.github/workflows/unity-testsuite-bindings.yml index 442abcd1..edb55783 100644 --- a/.github/workflows/unity-testsuite-bindings.yml +++ b/.github/workflows/unity-testsuite-bindings.yml @@ -16,7 +16,7 @@ jobs: - name: Checkout submodule run: | git submodule init - git submodule update + git submodule update --recursive cd unity-tests~ git checkout jdetter/circle-game-testsuite # Grab the branch name from the PR description. If it's not found, master will be used instead. From 43d9b8f40af58a47def063c7aa65f31e846c26bc Mon Sep 17 00:00:00 2001 From: Zeke Foppa <bfops@users.noreply.github.com> Date: Wed, 6 Nov 2024 14:51:15 -0800 Subject: [PATCH 51/55] fix unity tests? --- .github/workflows/unity-test.yml | 2 +- .github/workflows/unity-testsuite-bindings.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/unity-test.yml b/.github/workflows/unity-test.yml index c0623c9b..2f41ac81 100644 --- a/.github/workflows/unity-test.yml +++ b/.github/workflows/unity-test.yml @@ -98,7 +98,7 @@ jobs: - name: Install SpacetimeDB CLI from specific branch run: | cd unity-tests~ - git clone https://github.com/clockworklabs/SpacetimeDB.git + git clone --recurse-submodules https://github.com/clockworklabs/SpacetimeDB.git cd SpacetimeDB # Sanitize the branch name by trimming any newlines or spaces branch_name=$(echo "${{ steps.extract-branch.outputs.branch }}" | tr -d '[:space:]') diff --git a/.github/workflows/unity-testsuite-bindings.yml b/.github/workflows/unity-testsuite-bindings.yml index edb55783..fb063fb2 100644 --- a/.github/workflows/unity-testsuite-bindings.yml +++ b/.github/workflows/unity-testsuite-bindings.yml @@ -90,7 +90,7 @@ jobs: - name: Install SpacetimeDB CLI from specific branch run: | cd unity-tests~ - git clone https://github.com/clockworklabs/SpacetimeDB.git + git clone --recurse-submodules https://github.com/clockworklabs/SpacetimeDB.git cd SpacetimeDB # Sanitize the branch name by trimming any newlines or spaces branch_name=$(echo "${{ steps.extract-branch.outputs.branch }}" | tr -d '[:space:]') From a12594a4d39d80f37aae882a43e6e73a3a7dc94b Mon Sep 17 00:00:00 2001 From: Zeke Foppa <bfops@users.noreply.github.com> Date: Wed, 6 Nov 2024 15:01:31 -0800 Subject: [PATCH 52/55] fix unit tests? --- .github/workflows/unity-test.yml | 4 +++- .github/workflows/unity-testsuite-bindings.yml | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/unity-test.yml b/.github/workflows/unity-test.yml index 2f41ac81..0f59654e 100644 --- a/.github/workflows/unity-test.yml +++ b/.github/workflows/unity-test.yml @@ -98,7 +98,7 @@ jobs: - name: Install SpacetimeDB CLI from specific branch run: | cd unity-tests~ - git clone --recurse-submodules https://github.com/clockworklabs/SpacetimeDB.git + git clone https://github.com/clockworklabs/SpacetimeDB.git cd SpacetimeDB # Sanitize the branch name by trimming any newlines or spaces branch_name=$(echo "${{ steps.extract-branch.outputs.branch }}" | tr -d '[:space:]') @@ -107,6 +107,8 @@ jobs: branch_name="master" fi git checkout "$branch_name" + git submodule init + git submodule update --recursive echo "Checked out branch: $branch_name" cargo build --release -p spacetimedb-cli sudo mv target/release/spacetime /usr/bin/spacetime diff --git a/.github/workflows/unity-testsuite-bindings.yml b/.github/workflows/unity-testsuite-bindings.yml index fb063fb2..08fa5f9b 100644 --- a/.github/workflows/unity-testsuite-bindings.yml +++ b/.github/workflows/unity-testsuite-bindings.yml @@ -90,7 +90,7 @@ jobs: - name: Install SpacetimeDB CLI from specific branch run: | cd unity-tests~ - git clone --recurse-submodules https://github.com/clockworklabs/SpacetimeDB.git + git clone https://github.com/clockworklabs/SpacetimeDB.git cd SpacetimeDB # Sanitize the branch name by trimming any newlines or spaces branch_name=$(echo "${{ steps.extract-branch.outputs.branch }}" | tr -d '[:space:]') @@ -99,6 +99,8 @@ jobs: branch_name="master" fi git checkout "$branch_name" + git submodule init + git submodule update --recursive echo "Checked out branch: $branch_name" cargo build --release -p spacetimedb-cli sudo mv target/release/spacetime /usr/bin/spacetime From ecffc4a495aaad8dbd1ab93b706ddbfd07d18f24 Mon Sep 17 00:00:00 2001 From: Zeke Foppa <bfops@users.noreply.github.com> Date: Wed, 6 Nov 2024 15:20:06 -0800 Subject: [PATCH 53/55] empty From a7e0fe56cc9f3829c3ddf9b9be55a16d710f7fa1 Mon Sep 17 00:00:00 2001 From: Zeke Foppa <bfops@users.noreply.github.com> Date: Mon, 11 Nov 2024 18:04:42 -0800 Subject: [PATCH 54/55] updates --- SpacetimeDB.ClientSDK.csproj | 4 ++-- ...{1.0.0-rc1.meta => 1.0.0-rc1-hotfix1.meta} | 0 .../analyzers.meta | 0 .../analyzers/dotnet.meta | 0 .../analyzers/dotnet/cs.meta | 0 .../dotnet/cs/SpacetimeDB.BSATN.Codegen.dll | Bin 57344 -> 57344 bytes .../cs/SpacetimeDB.BSATN.Codegen.dll.meta | 0 .../{1.0.0-rc1 => 1.0.0-rc1-hotfix1}/lib.meta | 0 .../lib/netstandard2.1.meta | 0 .../SpacetimeDB.BSATN.Runtime.dll | Bin 0 -> 66048 bytes .../SpacetimeDB.BSATN.Runtime.dll.meta | 0 .../SpacetimeDB.BSATN.Runtime.dll | Bin 64512 -> 0 bytes 12 files changed, 2 insertions(+), 2 deletions(-) rename packages/spacetimedb.bsatn.runtime/{1.0.0-rc1.meta => 1.0.0-rc1-hotfix1.meta} (100%) rename packages/spacetimedb.bsatn.runtime/{1.0.0-rc1 => 1.0.0-rc1-hotfix1}/analyzers.meta (100%) rename packages/spacetimedb.bsatn.runtime/{1.0.0-rc1 => 1.0.0-rc1-hotfix1}/analyzers/dotnet.meta (100%) rename packages/spacetimedb.bsatn.runtime/{1.0.0-rc1 => 1.0.0-rc1-hotfix1}/analyzers/dotnet/cs.meta (100%) rename packages/spacetimedb.bsatn.runtime/{1.0.0-rc1 => 1.0.0-rc1-hotfix1}/analyzers/dotnet/cs/SpacetimeDB.BSATN.Codegen.dll (91%) rename packages/spacetimedb.bsatn.runtime/{1.0.0-rc1 => 1.0.0-rc1-hotfix1}/analyzers/dotnet/cs/SpacetimeDB.BSATN.Codegen.dll.meta (100%) rename packages/spacetimedb.bsatn.runtime/{1.0.0-rc1 => 1.0.0-rc1-hotfix1}/lib.meta (100%) rename packages/spacetimedb.bsatn.runtime/{1.0.0-rc1 => 1.0.0-rc1-hotfix1}/lib/netstandard2.1.meta (100%) create mode 100755 packages/spacetimedb.bsatn.runtime/1.0.0-rc1-hotfix1/lib/netstandard2.1/SpacetimeDB.BSATN.Runtime.dll rename packages/spacetimedb.bsatn.runtime/{1.0.0-rc1 => 1.0.0-rc1-hotfix1}/lib/netstandard2.1/SpacetimeDB.BSATN.Runtime.dll.meta (100%) delete mode 100755 packages/spacetimedb.bsatn.runtime/1.0.0-rc1/lib/netstandard2.1/SpacetimeDB.BSATN.Runtime.dll diff --git a/SpacetimeDB.ClientSDK.csproj b/SpacetimeDB.ClientSDK.csproj index 6bf4edcf..ddd2f9b3 100644 --- a/SpacetimeDB.ClientSDK.csproj +++ b/SpacetimeDB.ClientSDK.csproj @@ -17,14 +17,14 @@ <PackageReadmeFile>README.md</PackageReadmeFile> <RepositoryUrl>https://github.com/clockworklabs/com.clockworklabs.spacetimedbsdk</RepositoryUrl> <AssemblyVersion>1.0.0</AssemblyVersion> - <Version>1.0.0-rc1</Version> + <Version>1.0.0-rc1-hotfix1</Version> <DefaultItemExcludes>$(DefaultItemExcludes);*~/**</DefaultItemExcludes> <!-- We want to save DLLs for Unity which doesn't support NuGet. --> <RestorePackagesPath>packages</RestorePackagesPath> </PropertyGroup> <ItemGroup> - <PackageReference Include="SpacetimeDB.BSATN.Runtime" Version="1.0.0-rc1" /> + <PackageReference Include="SpacetimeDB.BSATN.Runtime" Version="1.0.0-rc1-hotfix1" /> <InternalsVisibleTo Include="SpacetimeDB.Tests" /> </ItemGroup> diff --git a/packages/spacetimedb.bsatn.runtime/1.0.0-rc1.meta b/packages/spacetimedb.bsatn.runtime/1.0.0-rc1-hotfix1.meta similarity index 100% rename from packages/spacetimedb.bsatn.runtime/1.0.0-rc1.meta rename to packages/spacetimedb.bsatn.runtime/1.0.0-rc1-hotfix1.meta diff --git a/packages/spacetimedb.bsatn.runtime/1.0.0-rc1/analyzers.meta b/packages/spacetimedb.bsatn.runtime/1.0.0-rc1-hotfix1/analyzers.meta similarity index 100% rename from packages/spacetimedb.bsatn.runtime/1.0.0-rc1/analyzers.meta rename to packages/spacetimedb.bsatn.runtime/1.0.0-rc1-hotfix1/analyzers.meta diff --git a/packages/spacetimedb.bsatn.runtime/1.0.0-rc1/analyzers/dotnet.meta b/packages/spacetimedb.bsatn.runtime/1.0.0-rc1-hotfix1/analyzers/dotnet.meta similarity index 100% rename from packages/spacetimedb.bsatn.runtime/1.0.0-rc1/analyzers/dotnet.meta rename to packages/spacetimedb.bsatn.runtime/1.0.0-rc1-hotfix1/analyzers/dotnet.meta diff --git a/packages/spacetimedb.bsatn.runtime/1.0.0-rc1/analyzers/dotnet/cs.meta b/packages/spacetimedb.bsatn.runtime/1.0.0-rc1-hotfix1/analyzers/dotnet/cs.meta similarity index 100% rename from packages/spacetimedb.bsatn.runtime/1.0.0-rc1/analyzers/dotnet/cs.meta rename to packages/spacetimedb.bsatn.runtime/1.0.0-rc1-hotfix1/analyzers/dotnet/cs.meta diff --git a/packages/spacetimedb.bsatn.runtime/1.0.0-rc1/analyzers/dotnet/cs/SpacetimeDB.BSATN.Codegen.dll b/packages/spacetimedb.bsatn.runtime/1.0.0-rc1-hotfix1/analyzers/dotnet/cs/SpacetimeDB.BSATN.Codegen.dll similarity index 91% rename from packages/spacetimedb.bsatn.runtime/1.0.0-rc1/analyzers/dotnet/cs/SpacetimeDB.BSATN.Codegen.dll rename to packages/spacetimedb.bsatn.runtime/1.0.0-rc1-hotfix1/analyzers/dotnet/cs/SpacetimeDB.BSATN.Codegen.dll index 5a2ceb283098edb4405a918b43bb8b7b3dcd675a..d90254a8922ee67bae5352ecc9ae8118877da17e 100755 GIT binary patch delta 1730 zcmXw)e{2(F7{{Nt*SWbLIM!~rUR&1fHs&0Ib)#&ffpxaYT$He62pL4v?%G?o2@@fP zAZA^PK;kqk4>87pcBw-!#(<sCX^34m6Vbnv1V(~bG7?NQA_0xY7=7L+qs@Kuz0dP~ zpL^a*@0!ReiL7$+g!QivP9K|`aEXs6&+l5*{|CT!kRJiXd6U()Vh<TQUH1TB2Uc6w zVZW#svp6p5Eya2Gq1Y+jKw+`v&Av-(@Zz<$Ot0mHPrRME-gd81youi#-L`X%^)Lc+ zqz7Qh(Fz(=VqLVW?Hlbi@>O<}L_0<5uusD)aK0?PqJ`{}a95k*WJV_OOjIqYaDL6? z3X}P0S4pL&3)xK<N!Wsq#TrB(cEogX7@s3OiQ_Td(dSHy*Qw_WCo`m#+5+W2!%H!> z@uqW`tM|DImM5qxEOH*jl7u@>#Uj^QEeJjMUrgQeuTy8z#hD;wYF$A~8g4p+%zG@d ze;;dPMP)aGlAB!yXATgrvM1<T4*NLwNm8{J_nWGt*Olf>miavEf}8}WO;yz5oax@P z#i=NADvu(ktyW}zT}guJ(wTQL2@;{%n1qxm%Je8TuttllC{INl9CkyI^B9yrg5E*B z<cKTDtJ1P3Mh4Zov}=LMWlB2im%0Bk_g`kNSd;KAo*C3__guPF&emX+{e#3S@yg&f zQHj;@{*pCXAW!m89!-MB<8IM_GjYGor`;m*j3ypI*iA|v@5j6H$25^&1Bm`1e_ex? z%9m@?tl50I_bmGb)|@0bHRKkZI5*U1OKA%Qa(4>_a(A~1Y9NJG!@BqggTosJ!|v)r z$!iO1AnXnl%FbO(LQKL;B21!8#@SD^zRx7fnRzCQL`K{>CbyW}mISX4cW<XTi)8=# zq8g~P=ZfUyIVMmnxhR(0Q!KfMNq~KTeTb`J_EGXF935E}Utr%0{;{w-TPiy&mdah^ z*xzOkt0lLtmfXWWz#3u_B^_}eWz8{%GMV?3@eJ$(>;vq>tO+Iw_9^MHWUpJijE#H! z^du(ivcplkd|D6e)yHTr<&q>=kCw~lBE>#iF0YbhzsNktB*z3Qc=8IF_f+saOaiQ- z3Iv}Ps;Gf+cbKbT=27CiwJ7t1Bx}l2T(GJLW13Z^8>e~HA{wZc{0O%0)9FXKYu{I- z>gWc#q1~g?jWkxBynL=sUO2}d)=J`8%cHOlNT0F2xc{lr)1l+HcPyTL@~u0`SJheT z#>}bD<BqMJ0ycZsH?MDQddcuM#YYdCLx;SL-hs$~VH)kej!nMyNK2dHZ5}X9v(;xb z$6{u*BeJo@N12XH%MX7l<;Q+9e+IFw*Fvx2Pv}U{>o|d1uBv%0^u}(%{;PWFvuA(# z{?h(EPp5vg*UxnNhjHTSOw$VdC%gh>qr1Jf<%_{{J0E}W+o$8S{NZN`pTFj$*ZJXV z6S!(=Ysm%c!wSF!>|c6Ml;Yn@y7)EYzOE|DJK)E*8?BKhh{Grx1QUkf5O|@Hjsb|! zVGuK*9emIMn@HP<wm=&(FEmr#<Zmlg3_6d|*Mul#Be0QjzLh>5nJ;f_5|924{6}=b Nmsx%Du9$4j{~t(7!)E{h delta 1735 zcmXw)ZERCz6vxlgTio26Wi4%b+q!m_L0|%1@7l{;Vp`X*5khz~0us%--gaX+kQW2^ z5;{9{5k62KK9DSE=a`BizR<+T2V-mT1L2Di9V{Vo$uOufz63Eb!RR?B+r7E}{`))s z=iKu&yLY*=k}E5x#_b<{7OECb`o(*N^V^%Y+y~eMbO?B0FNB;8K>-c;b?ILKPsxz& zF}YJL7Z>EHSZ;e@mmi9!#c?TY&P5mA#x=5X-6(CgjYq|)(yZ}kyLeq*P4_yB-qo-N zK1DqU-*|OU!6Dnsgi%(Hqi=CZBI;>WuWJ%shV#u+^IFF~4|mmRPL?Ps&zhdbCg0ag zt}&^YiAIO23F@YaJVfOqnH6G#?8<23fP5BpT#jZm?*`wLIDtK<Iaxw=s8!5=EWgNj z+Gl)oT)oFtu&u<Zu+cn<Ef2SRijCIVZ4wglzl>+seV@i8!I>V+`29V$DVXu~Fkf$@ z{@Ykn^Ge;)n2h+-oH>ZR#g)T*d0nI2Cy&)Wxj*ai_W7qcQ)XUat<ofNGV2k3c_|y% z74<0!O%+sV+K@v1H6;(v`8DPVCOt@SHYQ1B0yFECR&cASdF83FgV&W-Xr46Y56O-} zt?`gQ&#R`Wr%Vlc+9v!}CRZ@&bzR~9bKHN9xnj@5SMuzj=KR^O*=e>FcIw}Q+##<G zZWIm~8rs?DR^y9^Cl}!)@{OT@2+8T8Zs#iXHj<<&9+I#X6}`MSl&CqZiken9A-i{X zw}sSV4XvGGE!WWA%j~PH77;nIJ0J{sY4<kg8MRtVyQ|jH?rzt%!Wr2ztci27XL!xv zjzFl6_|m#o*b#`=QRf7cB$Iw7DJCY9QTCIp=a`f^Q(<x$NjhLLxy|Gb5ji{Dy9wv4 zr~Z}tR%mls^)$J~1R98o2I9d6;z1^H_Hp(}uJ*Gx(Vvm~_RNWka@+IW$94qD4(f2( zLA$Wn-(e4niQ5+w53-N5CYhM1>A)DP#T=R`A8h6s*vHw&+4r;NnB>?O$;-x(fY>YB zN4oJz%(<w;n2Vm)gCp88+{+RoIo7cy^js9#mzU68%Iq&Qx0qN=z|E7pDIaw6JWS%O zNw<VmYSP^bM+5y_?PqQx->RC-b3`6(E^@){k#JbGd+>)-gPwXEXes)%)c0!mN4b6P zm#Ch7Yw!o{-8b2eW3|!ktv0%`#U7Rs2`=MN*vHA2oG<L}bKF%g)%-N}?z+*r%&WgV zW4={7{rQkLW(bIMtPZaZuR4(KXpb01IGo9LnVoB69Rr<4IHG6u=ztN8g}V%0*R!3S zW-Mh6lpg=~Z>8gvLhl%ejhk)w3ciTf5qu3FnL#su2>f*7wH&XN`0idQQ!|?5CCfkj z+b7n#i{HPPe)3%CS9yA7I<f#G^N?Cce*ErM(yC18egD3H6qhd?l6<fba^uVWwd*Gf z;ab^r<B7(ieIWo)lsj*{BS8LhLlZYkfmx5Dya~EAZt7dwAp!=TVLUUCg)T6m6V^fu zI$!`DRU)8cMTaO712u-M3!Tnq7E_(LDh4Ukfzs8RF(Ko^@$etO|BXI~mKOi;yC{Tf F{s--Gxo7|Y diff --git a/packages/spacetimedb.bsatn.runtime/1.0.0-rc1/analyzers/dotnet/cs/SpacetimeDB.BSATN.Codegen.dll.meta b/packages/spacetimedb.bsatn.runtime/1.0.0-rc1-hotfix1/analyzers/dotnet/cs/SpacetimeDB.BSATN.Codegen.dll.meta similarity index 100% rename from packages/spacetimedb.bsatn.runtime/1.0.0-rc1/analyzers/dotnet/cs/SpacetimeDB.BSATN.Codegen.dll.meta rename to packages/spacetimedb.bsatn.runtime/1.0.0-rc1-hotfix1/analyzers/dotnet/cs/SpacetimeDB.BSATN.Codegen.dll.meta diff --git a/packages/spacetimedb.bsatn.runtime/1.0.0-rc1/lib.meta b/packages/spacetimedb.bsatn.runtime/1.0.0-rc1-hotfix1/lib.meta similarity index 100% rename from packages/spacetimedb.bsatn.runtime/1.0.0-rc1/lib.meta rename to packages/spacetimedb.bsatn.runtime/1.0.0-rc1-hotfix1/lib.meta diff --git a/packages/spacetimedb.bsatn.runtime/1.0.0-rc1/lib/netstandard2.1.meta b/packages/spacetimedb.bsatn.runtime/1.0.0-rc1-hotfix1/lib/netstandard2.1.meta similarity index 100% rename from packages/spacetimedb.bsatn.runtime/1.0.0-rc1/lib/netstandard2.1.meta rename to packages/spacetimedb.bsatn.runtime/1.0.0-rc1-hotfix1/lib/netstandard2.1.meta diff --git a/packages/spacetimedb.bsatn.runtime/1.0.0-rc1-hotfix1/lib/netstandard2.1/SpacetimeDB.BSATN.Runtime.dll b/packages/spacetimedb.bsatn.runtime/1.0.0-rc1-hotfix1/lib/netstandard2.1/SpacetimeDB.BSATN.Runtime.dll new file mode 100755 index 0000000000000000000000000000000000000000..a13680f9557f9ce320a0dd7603d91fcf30645eec GIT binary patch literal 66048 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 Prw`-M(QiKg|0M9=+-bOc literal 0 HcmV?d00001 diff --git a/packages/spacetimedb.bsatn.runtime/1.0.0-rc1/lib/netstandard2.1/SpacetimeDB.BSATN.Runtime.dll.meta b/packages/spacetimedb.bsatn.runtime/1.0.0-rc1-hotfix1/lib/netstandard2.1/SpacetimeDB.BSATN.Runtime.dll.meta similarity index 100% rename from packages/spacetimedb.bsatn.runtime/1.0.0-rc1/lib/netstandard2.1/SpacetimeDB.BSATN.Runtime.dll.meta rename to packages/spacetimedb.bsatn.runtime/1.0.0-rc1-hotfix1/lib/netstandard2.1/SpacetimeDB.BSATN.Runtime.dll.meta diff --git a/packages/spacetimedb.bsatn.runtime/1.0.0-rc1/lib/netstandard2.1/SpacetimeDB.BSATN.Runtime.dll b/packages/spacetimedb.bsatn.runtime/1.0.0-rc1/lib/netstandard2.1/SpacetimeDB.BSATN.Runtime.dll deleted file mode 100755 index c38d14a14d0b466f824e8f369693e0166125d1f8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 64512 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%0A9O@t8u>J@~^V2#mpVs>sX}utm z)}i_lUJw}Ji2r*Jf7s*Eo7?(umnYzFgw#b(;zd12@_BnhC|0I_T+n^K^~2Zx&ID&K zsN}c!BZ@WofAd6uYUg!bJIZfE`*wmRF5Pe@KbMvkmVST74=aE9**`gd@3TMsr_;x7 zT~{45HI4|+VEw8yvWfB1KZp$|H)_@7^5@tEXJ*2gHixF`m6;01m%{GMYMql+&GqiV z3Wv-(?HOw&Ba1V`A+;HAUS|>~y)nkgnzx&&={BZq>GV9(Hc_WbzS9Wi%Aa2>{o&MG zfBUJqfBi&Zv|t5o$NDj)OUYr!>3s=4V^&VIItz`)*>b&UHaKS62}64{&GWza%T)8A z2;;i`&!+Ttq~4q~{p~|=mIpWV5MF$z`MXl$|L9=>qfaf3k2EivCH6C4W}od6sYTu% zJx6|#>nYOud?)=c|K+qbnkEPJZ4COTO6hp=vp|Nov3aJ_p`N<k4b-0F8c%NY`M52$ z3{_8Ey3n<|$azR|?(>cG_w;O=KE->+57+dj<(Xx7-u)cmNfk0Mf5Y3X{0B<v+pv#_ zbnbybf%nvOZlFZ1dfub`MYXDjNDI7cTjFnt(*(<mzJxrqs~OR?_pb+E+Se4ntFZ+4 z1*D}@0UBH9fjYFSsmD{=pRe;)Ydp#s_BnMD;Bj=d*QB-+yiZT0!RLAVhJN$_PI9|R zKjVGR6SSe7(rf01?a4JtpMlPcTekcwe6Iv=fK#u=uwYx)-fNvvC1w9;*-R&7GyQ;U z3^M0Q!m#d_?WpvCFq+RpjA3^8XL#RuKm5`NJwogNC82mU|MNafBhLmu1U1^3VdQ!O zs9GhlHnP%4*O~twJahDBPCNR1s<toZJzY{)sYp)BX}-;4bmsl!68GsCZ}tXgO7c`i zJle<AZTMd_haW2Upmbx|9;E&X=MA1_Y)ARgn$WqA=NRJ=&PzNCZIzTLbJV=XpWebh z%Jo@r&yhN8f7akSLaApNeH9LUj@db$N1LRzdM2zVyd_q-W-y-ZxIdcnpMQR42@o47 Q_&lbs+xhwD|D6Q>8;j+CRsaA1 From f17ba3ef54b2575eb8db112f76363f1a624b7b91 Mon Sep 17 00:00:00 2001 From: Zeke Foppa <bfops@users.noreply.github.com> Date: Mon, 11 Nov 2024 18:05:27 -0800 Subject: [PATCH 55/55] empty commit to bump CI