diff --git a/.gitignore b/.gitignore index deee8429..a7df1c1a 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ ExportedObj/ *.csproj *.unityproj *.sln +*.slnx *.suo *.tmp *.user diff --git a/.vscode/.vstupatchdisable b/.vscode/.vstupatchdisable new file mode 100644 index 00000000..e69de29b diff --git a/Assets/Talo Game Services/Talo/Runtime/APIs/BaseAPI.cs b/Assets/Talo Game Services/Talo/Runtime/APIs/BaseAPI.cs index 4c30c30e..5d2ee337 100644 --- a/Assets/Talo Game Services/Talo/Runtime/APIs/BaseAPI.cs +++ b/Assets/Talo Game Services/Talo/Runtime/APIs/BaseAPI.cs @@ -9,7 +9,7 @@ namespace TaloGameServices public class BaseAPI { // automatically updated with a pre-commit hook - private const string ClientVersion = "1.1.0"; + private const string ClientVersion = "1.2.0"; protected string baseUrl; @@ -82,7 +82,7 @@ protected async Task Call( return HandleOfflineRequest(uri, method, content, allHeaders); } - byte[] json = new System.Text.UTF8Encoding().GetBytes(content); + byte[] json = RequestBodyEncoder.Encode(content, Talo.Settings.compressRequests, out var gzipped); using (UnityWebRequest www = new(uri, method)) { @@ -94,6 +94,11 @@ protected async Task Call( www.SetRequestHeader(header.key, header.value); } + if (gzipped) + { + www.SetRequestHeader("Content-Encoding", RequestBodyEncoder.GzipEncoding); + } + var op = www.SendWebRequest(); while (!op.isDone) diff --git a/Assets/Talo Game Services/Talo/Runtime/TaloSettings.cs b/Assets/Talo Game Services/Talo/Runtime/TaloSettings.cs index fa7522d2..a7033bb6 100644 --- a/Assets/Talo Game Services/Talo/Runtime/TaloSettings.cs +++ b/Assets/Talo Game Services/Talo/Runtime/TaloSettings.cs @@ -46,5 +46,8 @@ public class TaloSettings : ScriptableObject [Tooltip("The value for the verification key version")] public string verificationKeyValue = ""; + + [Tooltip("Gzip request bodies above 1KB")] + public bool compressRequests = true; } } diff --git a/Assets/Talo Game Services/Talo/Runtime/Utils/RequestBodyEncoder.cs b/Assets/Talo Game Services/Talo/Runtime/Utils/RequestBodyEncoder.cs new file mode 100644 index 00000000..191f5b1d --- /dev/null +++ b/Assets/Talo Game Services/Talo/Runtime/Utils/RequestBodyEncoder.cs @@ -0,0 +1,41 @@ +using System.IO; +using System.IO.Compression; +using System.Text; + +namespace TaloGameServices +{ + public static class RequestBodyEncoder + { + public const int MinCompressBytes = 1024; + + public const string GzipEncoding = "gzip"; + + public static byte[] Encode(string content, bool compress, out bool gzipped) + { + gzipped = false; + + if (string.IsNullOrEmpty(content)) + { + return new byte[0]; + } + + var raw = new UTF8Encoding().GetBytes(content); + + if (!compress || raw.Length < MinCompressBytes) + { + return raw; + } + + using (var output = new MemoryStream()) + { + using (var gzip = new GZipStream(output, CompressionLevel.Fastest)) + { + gzip.Write(raw, 0, raw.Length); + } + + gzipped = true; + return output.ToArray(); + } + } + } +} diff --git a/Assets/Talo Game Services/Talo/Runtime/Utils/RequestBodyEncoder.cs.meta b/Assets/Talo Game Services/Talo/Runtime/Utils/RequestBodyEncoder.cs.meta new file mode 100644 index 00000000..1e1a9cc2 --- /dev/null +++ b/Assets/Talo Game Services/Talo/Runtime/Utils/RequestBodyEncoder.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 540bfde1c42e94a8b953e76b2fc47b3a \ No newline at end of file diff --git a/Assets/Talo Game Services/Talo/Samples/FriendsDemo/Scripts/FriendsUIController.cs b/Assets/Talo Game Services/Talo/Samples/FriendsDemo/Scripts/FriendsUIController.cs index a05505e6..b01c7652 100644 --- a/Assets/Talo Game Services/Talo/Samples/FriendsDemo/Scripts/FriendsUIController.cs +++ b/Assets/Talo Game Services/Talo/Samples/FriendsDemo/Scripts/FriendsUIController.cs @@ -90,7 +90,7 @@ private async Task IdentifyPlayer() { playerName = GenerateRandomName(); await Talo.Players.Identify("username", playerName); - Talo.CurrentPlayer.SetProp("demo", "friendsDemo"); + await Talo.CurrentPlayer.SetProp("demo", "friendsDemo"); playerNameLabel.text = $"You are: {playerName}"; SendIntroFeedMessages(); diff --git a/Assets/Talo Game Services/Talo/Tests/RequestBodyEncoder.meta b/Assets/Talo Game Services/Talo/Tests/RequestBodyEncoder.meta new file mode 100644 index 00000000..64e57299 --- /dev/null +++ b/Assets/Talo Game Services/Talo/Tests/RequestBodyEncoder.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d43d50813e01b4d399208b4e309a2065 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Talo Game Services/Talo/Tests/RequestBodyEncoder/RequestBodyEncoderTest.cs b/Assets/Talo Game Services/Talo/Tests/RequestBodyEncoder/RequestBodyEncoderTest.cs new file mode 100644 index 00000000..c02f78f7 --- /dev/null +++ b/Assets/Talo Game Services/Talo/Tests/RequestBodyEncoder/RequestBodyEncoderTest.cs @@ -0,0 +1,111 @@ +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Text; +using NUnit.Framework; + +namespace TaloGameServices.Test +{ + internal class RequestBodyEncoderTest + { + // every gzip stream starts with 0x1F 0x8B + private const byte FirstGzipByte = 0x1F; + private const byte SecondGzipByte = 0x8B; + + private static readonly UTF8Encoding Utf8 = new(); + + private static string BuildJson(int size) + { + var padding = new string('a', size); + + return $"{{\"name\":\"event\",\"props\":[{{\"key\":\"pad\",\"value\":\"{padding}\"}}]}}"; + } + + private static byte[] Decompress(byte[] body) + { + using (var input = new MemoryStream(body)) + using (var gzip = new GZipStream(input, CompressionMode.Decompress)) + using (var output = new MemoryStream()) + { + gzip.CopyTo(output); + return output.ToArray(); + } + } + + [Test] + public void Encode_EmptyContent_ReturnsNoBytes() + { + var body = RequestBodyEncoder.Encode("", true, out var gzipped); + + Assert.AreEqual(0, body.Length); + Assert.IsFalse(gzipped); + } + + [Test] + public void Encode_SmallBody_IsNotCompressed() + { + var content = BuildJson(100); + + var body = RequestBodyEncoder.Encode(content, true, out var gzipped); + + Assert.IsFalse(gzipped); + CollectionAssert.AreEqual(Utf8.GetBytes(content), body); + } + + [Test] + public void Encode_LargeBody_StartsWithGzipHeader() + { + var body = RequestBodyEncoder.Encode(BuildJson(4096), true, out var gzipped); + + Assert.IsTrue(gzipped); + Assert.Greater(body.Length, 2); + Assert.AreEqual(FirstGzipByte, body[0]); + Assert.AreEqual(SecondGzipByte, body[1]); + } + + [Test] + public void Encode_LargeBody_DecompressesToOriginal() + { + var content = BuildJson(4096); + + var body = RequestBodyEncoder.Encode(content, true, out _); + + CollectionAssert.AreEqual(Utf8.GetBytes(content), Decompress(body)); + } + + [Test] + public void Encode_NonAsciiBody_DecompressesToOriginal() + { + // 2000 two-byte chars and an emoji: wrong encoding would break the round trip + var content = $"{{\"text\":\"{new string('\u00fc', 2000)} \U0001F600\"}}"; + + var body = RequestBodyEncoder.Encode(content, true, out var gzipped); + + Assert.IsTrue(gzipped); + CollectionAssert.AreEqual(Utf8.GetBytes(content), Decompress(body)); + } + + [Test] + public void Encode_CompressDisabled_IsNotCompressed() + { + var content = BuildJson(4096); + + var body = RequestBodyEncoder.Encode(content, false, out var gzipped); + + Assert.IsFalse(gzipped); + CollectionAssert.AreEqual(Utf8.GetBytes(content), body); + } + + [Test] + public void Encode_RepeatedEvents_IsSmallerThanInput() + { + var eventJson = BuildJson(380); + var content = $"[{string.Join(",", Enumerable.Repeat(eventJson, 50))}]"; + + var body = RequestBodyEncoder.Encode(content, true, out var gzipped); + + Assert.IsTrue(gzipped); + Assert.Less(body.Length, content.Length); + } + } +} diff --git a/Assets/Talo Game Services/Talo/Tests/RequestBodyEncoder/RequestBodyEncoderTest.cs.meta b/Assets/Talo Game Services/Talo/Tests/RequestBodyEncoder/RequestBodyEncoderTest.cs.meta new file mode 100644 index 00000000..4a4b1a48 --- /dev/null +++ b/Assets/Talo Game Services/Talo/Tests/RequestBodyEncoder/RequestBodyEncoderTest.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: bca254dc4687347aeb155d0d0bc7c149 \ No newline at end of file diff --git a/Assets/Talo Game Services/Talo/VERSION b/Assets/Talo Game Services/Talo/VERSION index 9084fa2f..26aaba0e 100644 --- a/Assets/Talo Game Services/Talo/VERSION +++ b/Assets/Talo Game Services/Talo/VERSION @@ -1 +1 @@ -1.1.0 +1.2.0 diff --git a/unity.slnx b/unity.slnx deleted file mode 100644 index 78ddfab3..00000000 --- a/unity.slnx +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - -