From 2a56d7ff8d1f51d7a2fc6b7a792733a77c7ff4ad Mon Sep 17 00:00:00 2001 From: tudor <7089284+tudddorrr@users.noreply.github.com> Date: Sat, 15 Nov 2025 00:00:14 +0000 Subject: [PATCH 1/2] debounce updating saves and players --- .../Talo/Runtime/APIs/DebouncedAPI.cs | 52 +++++++++++++++++++ .../Talo/Runtime/APIs/DebouncedAPI.cs.meta | 11 ++++ .../Talo/Runtime/APIs/PlayersAPI.cs | 22 +++++++- .../Talo/Runtime/APIs/SavesAPI.cs | 43 ++++++++++++++- .../Talo/Runtime/Entities/Player.cs | 9 ++-- .../Talo/Runtime/TaloManager.cs | 24 ++++++++- .../Playground/Scripts/Players/DeleteProp.cs | 8 +-- .../Playground/Scripts/Players/SetProp.cs | 8 +-- .../Tests/PlayersAPI/ClearIdentityTest.cs | 3 +- CLAUDE.md | 12 +++++ 10 files changed, 172 insertions(+), 20 deletions(-) create mode 100644 Assets/Talo Game Services/Talo/Runtime/APIs/DebouncedAPI.cs create mode 100644 Assets/Talo Game Services/Talo/Runtime/APIs/DebouncedAPI.cs.meta diff --git a/Assets/Talo Game Services/Talo/Runtime/APIs/DebouncedAPI.cs b/Assets/Talo Game Services/Talo/Runtime/APIs/DebouncedAPI.cs new file mode 100644 index 0000000..40f960a --- /dev/null +++ b/Assets/Talo Game Services/Talo/Runtime/APIs/DebouncedAPI.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using UnityEngine; + +namespace TaloGameServices +{ + public abstract class DebouncedAPI : BaseAPI where TOperation : Enum + { + private class DebouncedOperation + { + public float nextUpdateTime; + public bool hasPending; + } + + private readonly Dictionary operations = new(); + + protected DebouncedAPI(string service) : base(service) { } + + protected void Debounce(TOperation operation) + { + if (!operations.ContainsKey(operation)) + { + operations[operation] = new DebouncedOperation(); + } + + operations[operation].nextUpdateTime = Time.realtimeSinceStartup + Talo.Settings.debounceTimerSeconds; + operations[operation].hasPending = true; + } + + public async Task ProcessPendingUpdates() + { + var keysToProcess = new List(); + + foreach (var kvp in operations) + { + if (kvp.Value.hasPending && Time.realtimeSinceStartup >= kvp.Value.nextUpdateTime) + { + keysToProcess.Add(kvp.Key); + } + } + + foreach (var key in keysToProcess) + { + operations[key].hasPending = false; + await ExecuteDebouncedOperation(key); + } + } + + protected abstract Task ExecuteDebouncedOperation(TOperation operation); + } +} diff --git a/Assets/Talo Game Services/Talo/Runtime/APIs/DebouncedAPI.cs.meta b/Assets/Talo Game Services/Talo/Runtime/APIs/DebouncedAPI.cs.meta new file mode 100644 index 0000000..0bdbf4d --- /dev/null +++ b/Assets/Talo Game Services/Talo/Runtime/APIs/DebouncedAPI.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7c9d1e2f3a4b5c6d7e8f9a0b1c2d3e4f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Talo Game Services/Talo/Runtime/APIs/PlayersAPI.cs b/Assets/Talo Game Services/Talo/Runtime/APIs/PlayersAPI.cs index fbb0000..6b3d100 100644 --- a/Assets/Talo Game Services/Talo/Runtime/APIs/PlayersAPI.cs +++ b/Assets/Talo Game Services/Talo/Runtime/APIs/PlayersAPI.cs @@ -10,8 +10,13 @@ public class MergeOptions public string postMergeIdentityService = ""; } - public class PlayersAPI : BaseAPI + public class PlayersAPI : DebouncedAPI { + public enum DebouncedOperation + { + Update + } + public event Action OnIdentified; public event Action OnIdentificationStarted; public event Action OnIdentificationFailed; @@ -103,6 +108,21 @@ public async Task IdentifySteam(string ticket, string identity = "") return Talo.CurrentPlayer; } + protected override async Task ExecuteDebouncedOperation(DebouncedOperation operation) + { + switch (operation) + { + case DebouncedOperation.Update: + await Update(); + break; + } + } + + public void DebounceUpdate() + { + Debounce(DebouncedOperation.Update); + } + public async Task Update() { Talo.IdentityCheck(); diff --git a/Assets/Talo Game Services/Talo/Runtime/APIs/SavesAPI.cs b/Assets/Talo Game Services/Talo/Runtime/APIs/SavesAPI.cs index 81a9032..203c210 100644 --- a/Assets/Talo Game Services/Talo/Runtime/APIs/SavesAPI.cs +++ b/Assets/Talo Game Services/Talo/Runtime/APIs/SavesAPI.cs @@ -6,8 +6,13 @@ namespace TaloGameServices { - public class SavesAPI : BaseAPI + public class SavesAPI : DebouncedAPI { + public enum DebouncedOperation + { + Update + } + internal SavesManager savesManager; internal SavesContentManager contentManager; @@ -186,9 +191,43 @@ public async Task CreateSave(string saveName, SaveContent content = nu return savesManager.CreateSave(save); } + protected override async Task ExecuteDebouncedOperation(DebouncedOperation operation) + { + switch (operation) + { + case DebouncedOperation.Update: + var currentSave = savesManager.CurrentSave; + if (currentSave != null) + { + await UpdateSave(currentSave.id); + } + break; + } + } + + public void DebounceUpdate() + { + Debounce(DebouncedOperation.Update); + } + public async Task UpdateCurrentSave(string newName = "") { - return await UpdateSave(savesManager.CurrentSave.id, newName); + var currentSave = savesManager.CurrentSave; + if (currentSave == null) + { + throw new Exception("No save is currently loaded"); + } + + // if the save is being renamed, sync it immediately + if (!string.IsNullOrEmpty(newName)) + { + return await UpdateSave(currentSave.id, newName); + } + + // else, update the save locally and queue it for syncing + currentSave.content = contentManager.Content; + DebounceUpdate(); + return currentSave; } public async Task UpdateSave(int saveId, string newName = "") diff --git a/Assets/Talo Game Services/Talo/Runtime/Entities/Player.cs b/Assets/Talo Game Services/Talo/Runtime/Entities/Player.cs index 694dddc..30d8464 100644 --- a/Assets/Talo Game Services/Talo/Runtime/Entities/Player.cs +++ b/Assets/Talo Game Services/Talo/Runtime/Entities/Player.cs @@ -1,7 +1,6 @@ using UnityEngine; using System.Linq; using System; -using System.Threading.Tasks; namespace TaloGameServices { @@ -17,23 +16,23 @@ public override string ToString() return JsonUtility.ToJson(this); } - public async Task SetProp(string key, string value, bool update = true) + public void SetProp(string key, string value, bool update = true) { base.SetProp(key, value); if (update) { - await Talo.Players.Update(); + Talo.Players.DebounceUpdate(); } } - public async Task DeleteProp(string key, bool update = true) + public void DeleteProp(string key, bool update = true) { base.DeleteProp(key); if (update) { - await Talo.Players.Update(); + Talo.Players.DebounceUpdate(); } } diff --git a/Assets/Talo Game Services/Talo/Runtime/TaloManager.cs b/Assets/Talo Game Services/Talo/Runtime/TaloManager.cs index d8056d1..2962d12 100644 --- a/Assets/Talo Game Services/Talo/Runtime/TaloManager.cs +++ b/Assets/Talo Game Services/Talo/Runtime/TaloManager.cs @@ -48,7 +48,7 @@ private async void DoFlush() } } - private async void Update() + private void Update() { if (Application.platform == RuntimePlatform.WebGLPlayer) { @@ -65,10 +65,30 @@ private async void Update() tmrContinuity += Time.deltaTime; if (tmrContinuity >= 10f) { - await Talo.Continuity.ProcessRequests(); + ProcessContinuityRequests(); tmrContinuity = 0; } } + + ProcessDebouncedUpdates(); + } + + private async void ProcessContinuityRequests() + { + await Talo.Continuity.ProcessRequests(); + } + + private async void ProcessDebouncedUpdates() + { + if (Talo.HasIdentity()) + { + await Talo.Players.ProcessPendingUpdates(); + } + + if (Talo.Saves.Current != null) + { + await Talo.Saves.ProcessPendingUpdates(); + } } public void ResetFlushTimer() diff --git a/Assets/Talo Game Services/Talo/Samples/Playground/Scripts/Players/DeleteProp.cs b/Assets/Talo Game Services/Talo/Samples/Playground/Scripts/Players/DeleteProp.cs index 527219a..57a6e4f 100644 --- a/Assets/Talo Game Services/Talo/Samples/Playground/Scripts/Players/DeleteProp.cs +++ b/Assets/Talo Game Services/Talo/Samples/Playground/Scripts/Players/DeleteProp.cs @@ -8,12 +8,12 @@ public class DeleteHealthProp : MonoBehaviour { public string key; - public async void OnButtonClick() + public void OnButtonClick() { - await DeleteProp(); + DeleteProp(); } - private async Task DeleteProp() + private void DeleteProp() { if (string.IsNullOrEmpty(key)) { @@ -23,7 +23,7 @@ private async Task DeleteProp() try { - await Talo.CurrentPlayer.DeleteProp(key); + Talo.CurrentPlayer.DeleteProp(key); ResponseMessage.SetText($"{key} deleted"); } catch (Exception ex) diff --git a/Assets/Talo Game Services/Talo/Samples/Playground/Scripts/Players/SetProp.cs b/Assets/Talo Game Services/Talo/Samples/Playground/Scripts/Players/SetProp.cs index 4a785ad..ea9445a 100644 --- a/Assets/Talo Game Services/Talo/Samples/Playground/Scripts/Players/SetProp.cs +++ b/Assets/Talo Game Services/Talo/Samples/Playground/Scripts/Players/SetProp.cs @@ -7,12 +7,12 @@ public class SetProp : MonoBehaviour { public string key, value; - public async void OnButtonClick() + public void OnButtonClick() { - await UpdateProp(); + UpdateProp(); } - private async Task UpdateProp() + private void UpdateProp() { if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(value)) { @@ -22,7 +22,7 @@ private async Task UpdateProp() try { - await Talo.CurrentPlayer.SetProp(key, value); + Talo.CurrentPlayer.SetProp(key, value); ResponseMessage.SetText($"{key} set to {value}"); } catch (System.Exception ex) diff --git a/Assets/Talo Game Services/Talo/Tests/PlayersAPI/ClearIdentityTest.cs b/Assets/Talo Game Services/Talo/Tests/PlayersAPI/ClearIdentityTest.cs index 4618cc5..acc0355 100644 --- a/Assets/Talo Game Services/Talo/Tests/PlayersAPI/ClearIdentityTest.cs +++ b/Assets/Talo Game Services/Talo/Tests/PlayersAPI/ClearIdentityTest.cs @@ -2,7 +2,6 @@ using NUnit.Framework; using UnityEngine.TestTools; using UnityEngine; -using System.Threading.Tasks; namespace TaloGameServices.Test { @@ -41,7 +40,7 @@ public IEnumerator ClearIdentity_ShouldClearAliasData() yield return Talo.Events.Track("test-event"); Assert.IsNotEmpty(Talo.Events.queue); - Talo.Players.ClearIdentity(); + _ = Talo.Players.ClearIdentity(); Assert.IsNull(Talo.CurrentAlias); Assert.IsTrue(eventMock.identityCleared); Assert.IsEmpty(Talo.Events.queue); diff --git a/CLAUDE.md b/CLAUDE.md index 9699930..2e506f7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -46,6 +46,18 @@ The SDK supports offline operation via `TaloSettings.offlineMode`. When offline, ### Event Flushing Events are batched and flushed on application quit/pause/focus loss. On WebGL, events flush every `webGLEventFlushRate` seconds (default 30s) due to platform limitations. +### Debouncing +Player updates and save updates are debounced to prevent excessive API calls during rapid property changes. APIs that need debouncing inherit from `DebouncedAPI` (a generic base class) and define a `DebouncedOperation` enum for type-safe operation keys. The base class uses a dictionary to track multiple debounced operations independently. + +To add debouncing to an API: +1. Define a public `enum DebouncedOperation` with your debounced operations +2. Inherit from `DebouncedAPI` +3. Call `Debounce(DebouncedOperation.YourOperation)` to queue an operation +4. Implement `ExecuteDebouncedOperation(DebouncedOperation operation)` with a switch statement +5. The base class's `ProcessPendingUpdates()` is called by `TaloManager.Update()` every frame + +Example: `PlayersAPI` defines `enum DebouncedOperation { Update }` and inherits from `DebouncedAPI`. When `Player.SetProp()` is called, it calls `Debounce(DebouncedOperation.Update)`, which queues the update to be executed after `debounceTimerSeconds` (default: 1s). Multiple property changes within the debounce window result in a single API call. + ## Common Development Commands ### Running Tests From 1917ef8ce904b851def67cd8b82362d9cb7f95a4 Mon Sep 17 00:00:00 2001 From: tudor <7089284+tudddorrr@users.noreply.github.com> Date: Sat, 6 Dec 2025 20:06:07 +0000 Subject: [PATCH 2/2] error handling around async operations --- .../Talo/Runtime/TaloManager.cs | 38 +++++++++++++++---- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/Assets/Talo Game Services/Talo/Runtime/TaloManager.cs b/Assets/Talo Game Services/Talo/Runtime/TaloManager.cs index 2962d12..b84a689 100644 --- a/Assets/Talo Game Services/Talo/Runtime/TaloManager.cs +++ b/Assets/Talo Game Services/Talo/Runtime/TaloManager.cs @@ -1,3 +1,4 @@ +using System; using UnityEngine; namespace TaloGameServices @@ -42,9 +43,16 @@ private void OnApplicationPause(bool isPaused) private async void DoFlush() { - if (Talo.HasIdentity()) + try { - await Talo.Events.Flush(); + if (Talo.HasIdentity()) + { + await Talo.Events.Flush(); + } + } + catch (Exception ex) + { + Debug.LogError($"Failed to flush events: {ex}"); } } @@ -75,19 +83,33 @@ private void Update() private async void ProcessContinuityRequests() { - await Talo.Continuity.ProcessRequests(); + try + { + await Talo.Continuity.ProcessRequests(); + } + catch (Exception ex) + { + Debug.LogError($"Failed to process continuity requests: {ex}"); + } } private async void ProcessDebouncedUpdates() { - if (Talo.HasIdentity()) + try { - await Talo.Players.ProcessPendingUpdates(); - } + if (Talo.HasIdentity()) + { + await Talo.Players.ProcessPendingUpdates(); + } - if (Talo.Saves.Current != null) + if (Talo.Saves.Current != null) + { + await Talo.Saves.ProcessPendingUpdates(); + } + } + catch (Exception ex) { - await Talo.Saves.ProcessPendingUpdates(); + Debug.LogError($"Failed to process debounced updates: {ex}"); } }