Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions Assets/Talo Game Services/Talo/Runtime/APIs/DebouncedAPI.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine;

namespace TaloGameServices
{
public abstract class DebouncedAPI<TOperation> : BaseAPI where TOperation : Enum
{
private class DebouncedOperation
{
public float nextUpdateTime;
public bool hasPending;
}

private readonly Dictionary<TOperation, DebouncedOperation> 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<TOperation>();

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);
}
}
11 changes: 11 additions & 0 deletions Assets/Talo Game Services/Talo/Runtime/APIs/DebouncedAPI.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 21 additions & 1 deletion Assets/Talo Game Services/Talo/Runtime/APIs/PlayersAPI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,13 @@ public class MergeOptions
public string postMergeIdentityService = "";
}

public class PlayersAPI : BaseAPI
public class PlayersAPI : DebouncedAPI<PlayersAPI.DebouncedOperation>
{
public enum DebouncedOperation
{
Update
}

public event Action<Player> OnIdentified;
public event Action OnIdentificationStarted;
public event Action OnIdentificationFailed;
Expand Down Expand Up @@ -103,6 +108,21 @@ public async Task<Player> 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<Player> Update()
{
Talo.IdentityCheck();
Expand Down
43 changes: 41 additions & 2 deletions Assets/Talo Game Services/Talo/Runtime/APIs/SavesAPI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,13 @@

namespace TaloGameServices
{
public class SavesAPI : BaseAPI
public class SavesAPI : DebouncedAPI<SavesAPI.DebouncedOperation>
{
public enum DebouncedOperation
{
Update
}

internal SavesManager savesManager;
internal SavesContentManager contentManager;

Expand Down Expand Up @@ -186,9 +191,43 @@ public async Task<GameSave> 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<GameSave> 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<GameSave> UpdateSave(int saveId, string newName = "")
Expand Down
9 changes: 4 additions & 5 deletions Assets/Talo Game Services/Talo/Runtime/Entities/Player.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using UnityEngine;
using System.Linq;
using System;
using System.Threading.Tasks;

namespace TaloGameServices
{
Expand All @@ -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();
}
}

Expand Down
50 changes: 46 additions & 4 deletions Assets/Talo Game Services/Talo/Runtime/TaloManager.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using UnityEngine;

namespace TaloGameServices
Expand Down Expand Up @@ -42,13 +43,20 @@ 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}");
}
}

private async void Update()
private void Update()
{
if (Application.platform == RuntimePlatform.WebGLPlayer)
{
Expand All @@ -65,10 +73,44 @@ private async void Update()
tmrContinuity += Time.deltaTime;
if (tmrContinuity >= 10f)
{
await Talo.Continuity.ProcessRequests();
ProcessContinuityRequests();
tmrContinuity = 0;
}
}

ProcessDebouncedUpdates();
}

private async void ProcessContinuityRequests()
{
try
{
await Talo.Continuity.ProcessRequests();
}
catch (Exception ex)
{
Debug.LogError($"Failed to process continuity requests: {ex}");
}
}

private async void ProcessDebouncedUpdates()
{
try
{
if (Talo.HasIdentity())
{
await Talo.Players.ProcessPendingUpdates();
}

if (Talo.Saves.Current != null)
{
await Talo.Saves.ProcessPendingUpdates();
}
}
catch (Exception ex)
{
Debug.LogError($"Failed to process debounced updates: {ex}");
}
}

public void ResetFlushTimer()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
{
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
{
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
using NUnit.Framework;
using UnityEngine.TestTools;
using UnityEngine;
using System.Threading.Tasks;

namespace TaloGameServices.Test
{
Expand Down Expand Up @@ -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);
Expand Down
12 changes: 12 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<TOperation>` (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<YourAPI.DebouncedOperation>`
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<PlayersAPI.DebouncedOperation>`. 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
Expand Down