Skip to content

Commit 91fcf7d

Browse files
authored
Merge pull request #166 from TaloDev/debounce-players-saves
Debounce updating saves and players
2 parents 554b207 + 1917ef8 commit 91fcf7d

File tree

10 files changed

+196
-22
lines changed

10 files changed

+196
-22
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Threading.Tasks;
4+
using UnityEngine;
5+
6+
namespace TaloGameServices
7+
{
8+
public abstract class DebouncedAPI<TOperation> : BaseAPI where TOperation : Enum
9+
{
10+
private class DebouncedOperation
11+
{
12+
public float nextUpdateTime;
13+
public bool hasPending;
14+
}
15+
16+
private readonly Dictionary<TOperation, DebouncedOperation> operations = new();
17+
18+
protected DebouncedAPI(string service) : base(service) { }
19+
20+
protected void Debounce(TOperation operation)
21+
{
22+
if (!operations.ContainsKey(operation))
23+
{
24+
operations[operation] = new DebouncedOperation();
25+
}
26+
27+
operations[operation].nextUpdateTime = Time.realtimeSinceStartup + Talo.Settings.debounceTimerSeconds;
28+
operations[operation].hasPending = true;
29+
}
30+
31+
public async Task ProcessPendingUpdates()
32+
{
33+
var keysToProcess = new List<TOperation>();
34+
35+
foreach (var kvp in operations)
36+
{
37+
if (kvp.Value.hasPending && Time.realtimeSinceStartup >= kvp.Value.nextUpdateTime)
38+
{
39+
keysToProcess.Add(kvp.Key);
40+
}
41+
}
42+
43+
foreach (var key in keysToProcess)
44+
{
45+
operations[key].hasPending = false;
46+
await ExecuteDebouncedOperation(key);
47+
}
48+
}
49+
50+
protected abstract Task ExecuteDebouncedOperation(TOperation operation);
51+
}
52+
}

Assets/Talo Game Services/Talo/Runtime/APIs/DebouncedAPI.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Assets/Talo Game Services/Talo/Runtime/APIs/PlayersAPI.cs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,13 @@ public class MergeOptions
1010
public string postMergeIdentityService = "";
1111
}
1212

13-
public class PlayersAPI : BaseAPI
13+
public class PlayersAPI : DebouncedAPI<PlayersAPI.DebouncedOperation>
1414
{
15+
public enum DebouncedOperation
16+
{
17+
Update
18+
}
19+
1520
public event Action<Player> OnIdentified;
1621
public event Action OnIdentificationStarted;
1722
public event Action OnIdentificationFailed;
@@ -103,6 +108,21 @@ public async Task<Player> IdentifySteam(string ticket, string identity = "")
103108
return Talo.CurrentPlayer;
104109
}
105110

111+
protected override async Task ExecuteDebouncedOperation(DebouncedOperation operation)
112+
{
113+
switch (operation)
114+
{
115+
case DebouncedOperation.Update:
116+
await Update();
117+
break;
118+
}
119+
}
120+
121+
public void DebounceUpdate()
122+
{
123+
Debounce(DebouncedOperation.Update);
124+
}
125+
106126
public async Task<Player> Update()
107127
{
108128
Talo.IdentityCheck();

Assets/Talo Game Services/Talo/Runtime/APIs/SavesAPI.cs

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,13 @@
66

77
namespace TaloGameServices
88
{
9-
public class SavesAPI : BaseAPI
9+
public class SavesAPI : DebouncedAPI<SavesAPI.DebouncedOperation>
1010
{
11+
public enum DebouncedOperation
12+
{
13+
Update
14+
}
15+
1116
internal SavesManager savesManager;
1217
internal SavesContentManager contentManager;
1318

@@ -186,9 +191,43 @@ public async Task<GameSave> CreateSave(string saveName, SaveContent content = nu
186191
return savesManager.CreateSave(save);
187192
}
188193

194+
protected override async Task ExecuteDebouncedOperation(DebouncedOperation operation)
195+
{
196+
switch (operation)
197+
{
198+
case DebouncedOperation.Update:
199+
var currentSave = savesManager.CurrentSave;
200+
if (currentSave != null)
201+
{
202+
await UpdateSave(currentSave.id);
203+
}
204+
break;
205+
}
206+
}
207+
208+
public void DebounceUpdate()
209+
{
210+
Debounce(DebouncedOperation.Update);
211+
}
212+
189213
public async Task<GameSave> UpdateCurrentSave(string newName = "")
190214
{
191-
return await UpdateSave(savesManager.CurrentSave.id, newName);
215+
var currentSave = savesManager.CurrentSave;
216+
if (currentSave == null)
217+
{
218+
throw new Exception("No save is currently loaded");
219+
}
220+
221+
// if the save is being renamed, sync it immediately
222+
if (!string.IsNullOrEmpty(newName))
223+
{
224+
return await UpdateSave(currentSave.id, newName);
225+
}
226+
227+
// else, update the save locally and queue it for syncing
228+
currentSave.content = contentManager.Content;
229+
DebounceUpdate();
230+
return currentSave;
192231
}
193232

194233
public async Task<GameSave> UpdateSave(int saveId, string newName = "")

Assets/Talo Game Services/Talo/Runtime/Entities/Player.cs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
using UnityEngine;
22
using System.Linq;
33
using System;
4-
using System.Threading.Tasks;
54

65
namespace TaloGameServices
76
{
@@ -17,23 +16,23 @@ public override string ToString()
1716
return JsonUtility.ToJson(this);
1817
}
1918

20-
public async Task SetProp(string key, string value, bool update = true)
19+
public void SetProp(string key, string value, bool update = true)
2120
{
2221
base.SetProp(key, value);
2322

2423
if (update)
2524
{
26-
await Talo.Players.Update();
25+
Talo.Players.DebounceUpdate();
2726
}
2827
}
2928

30-
public async Task DeleteProp(string key, bool update = true)
29+
public void DeleteProp(string key, bool update = true)
3130
{
3231
base.DeleteProp(key);
3332

3433
if (update)
3534
{
36-
await Talo.Players.Update();
35+
Talo.Players.DebounceUpdate();
3736
}
3837
}
3938

Assets/Talo Game Services/Talo/Runtime/TaloManager.cs

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using UnityEngine;
23

34
namespace TaloGameServices
@@ -42,13 +43,20 @@ private void OnApplicationPause(bool isPaused)
4243

4344
private async void DoFlush()
4445
{
45-
if (Talo.HasIdentity())
46+
try
4647
{
47-
await Talo.Events.Flush();
48+
if (Talo.HasIdentity())
49+
{
50+
await Talo.Events.Flush();
51+
}
52+
}
53+
catch (Exception ex)
54+
{
55+
Debug.LogError($"Failed to flush events: {ex}");
4856
}
4957
}
5058

51-
private async void Update()
59+
private void Update()
5260
{
5361
if (Application.platform == RuntimePlatform.WebGLPlayer)
5462
{
@@ -65,10 +73,44 @@ private async void Update()
6573
tmrContinuity += Time.deltaTime;
6674
if (tmrContinuity >= 10f)
6775
{
68-
await Talo.Continuity.ProcessRequests();
76+
ProcessContinuityRequests();
6977
tmrContinuity = 0;
7078
}
7179
}
80+
81+
ProcessDebouncedUpdates();
82+
}
83+
84+
private async void ProcessContinuityRequests()
85+
{
86+
try
87+
{
88+
await Talo.Continuity.ProcessRequests();
89+
}
90+
catch (Exception ex)
91+
{
92+
Debug.LogError($"Failed to process continuity requests: {ex}");
93+
}
94+
}
95+
96+
private async void ProcessDebouncedUpdates()
97+
{
98+
try
99+
{
100+
if (Talo.HasIdentity())
101+
{
102+
await Talo.Players.ProcessPendingUpdates();
103+
}
104+
105+
if (Talo.Saves.Current != null)
106+
{
107+
await Talo.Saves.ProcessPendingUpdates();
108+
}
109+
}
110+
catch (Exception ex)
111+
{
112+
Debug.LogError($"Failed to process debounced updates: {ex}");
113+
}
72114
}
73115

74116
public void ResetFlushTimer()

Assets/Talo Game Services/Talo/Samples/Playground/Scripts/Players/DeleteProp.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@ public class DeleteHealthProp : MonoBehaviour
88
{
99
public string key;
1010

11-
public async void OnButtonClick()
11+
public void OnButtonClick()
1212
{
13-
await DeleteProp();
13+
DeleteProp();
1414
}
1515

16-
private async Task DeleteProp()
16+
private void DeleteProp()
1717
{
1818
if (string.IsNullOrEmpty(key))
1919
{
@@ -23,7 +23,7 @@ private async Task DeleteProp()
2323

2424
try
2525
{
26-
await Talo.CurrentPlayer.DeleteProp(key);
26+
Talo.CurrentPlayer.DeleteProp(key);
2727
ResponseMessage.SetText($"{key} deleted");
2828
}
2929
catch (Exception ex)

Assets/Talo Game Services/Talo/Samples/Playground/Scripts/Players/SetProp.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ public class SetProp : MonoBehaviour
77
{
88
public string key, value;
99

10-
public async void OnButtonClick()
10+
public void OnButtonClick()
1111
{
12-
await UpdateProp();
12+
UpdateProp();
1313
}
1414

15-
private async Task UpdateProp()
15+
private void UpdateProp()
1616
{
1717
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(value))
1818
{
@@ -22,7 +22,7 @@ private async Task UpdateProp()
2222

2323
try
2424
{
25-
await Talo.CurrentPlayer.SetProp(key, value);
25+
Talo.CurrentPlayer.SetProp(key, value);
2626
ResponseMessage.SetText($"{key} set to {value}");
2727
}
2828
catch (System.Exception ex)

Assets/Talo Game Services/Talo/Tests/PlayersAPI/ClearIdentityTest.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
using NUnit.Framework;
33
using UnityEngine.TestTools;
44
using UnityEngine;
5-
using System.Threading.Tasks;
65

76
namespace TaloGameServices.Test
87
{
@@ -41,7 +40,7 @@ public IEnumerator ClearIdentity_ShouldClearAliasData()
4140
yield return Talo.Events.Track("test-event");
4241
Assert.IsNotEmpty(Talo.Events.queue);
4342

44-
Talo.Players.ClearIdentity();
43+
_ = Talo.Players.ClearIdentity();
4544
Assert.IsNull(Talo.CurrentAlias);
4645
Assert.IsTrue(eventMock.identityCleared);
4746
Assert.IsEmpty(Talo.Events.queue);

CLAUDE.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,18 @@ The SDK supports offline operation via `TaloSettings.offlineMode`. When offline,
4646
### Event Flushing
4747
Events are batched and flushed on application quit/pause/focus loss. On WebGL, events flush every `webGLEventFlushRate` seconds (default 30s) due to platform limitations.
4848

49+
### Debouncing
50+
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.
51+
52+
To add debouncing to an API:
53+
1. Define a public `enum DebouncedOperation` with your debounced operations
54+
2. Inherit from `DebouncedAPI<YourAPI.DebouncedOperation>`
55+
3. Call `Debounce(DebouncedOperation.YourOperation)` to queue an operation
56+
4. Implement `ExecuteDebouncedOperation(DebouncedOperation operation)` with a switch statement
57+
5. The base class's `ProcessPendingUpdates()` is called by `TaloManager.Update()` every frame
58+
59+
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.
60+
4961
## Common Development Commands
5062

5163
### Running Tests

0 commit comments

Comments
 (0)