Skip to content
Open
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
18 changes: 18 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,24 @@ To add debouncing to an API:

Example: `PlayersAPI` defines `enum DebouncedOperation { Update }` and inherits from `DebouncedAPI<PlayersAPI.DebouncedOperation>`. When `Player.SetProp()` is called, it calls `Debounce(DebouncedOperation.Update)`. The first call fires immediately; subsequent calls within the debounce window result in a single trailing API call at the end of the window.

#### Debounced update completion signals

Because the trailing call is fired by `TaloManager.Update()` on a later frame, the caller's code (e.g. `Player.SetProp()`) returns long before the PATCH actually settles. Two complementary signals let the caller know when a batch has landed:

- **Event** — `Talo.Players.OnPlayerUpdated` / `Talo.Players.OnPlayerUpdateFailed` (and the matching `Talo.Saves.OnSaveUpdated(GameSave)` / `Talo.Saves.OnSaveUpdateFailed`) fire after each settled batch. `OnPlayerUpdated` and `OnPlayerUpdateFailed` are mutually exclusive. `OnPropsRejected` still fires independently when the server rejects props.
- **`await FlushUpdates()`** — `Talo.Players.FlushUpdates()` (and `Talo.Saves.FlushUpdates()`) force any pending trailing call to fire immediately and `await` it, returning `true` if anything was flushed and `false` if nothing was pending. Throws on transport failure (matching the SDK's `Identify` pattern: the event fires AND the `Task` throws). Use this when you need determinism, e.g. before quitting:

```csharp
private async void OnApplicationQuit()
{
if (Talo.Saves.Current != null)
{
try { await Talo.Saves.FlushUpdates(); }
catch (Exception ex) { Debug.LogError($"Final save failed: {ex.Message}"); }
}
}
```

## Key Configuration

### TaloSettings
Expand Down
82 changes: 74 additions & 8 deletions Assets/Talo Game Services/Talo/Runtime/APIs/DebouncedAPI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,14 @@ private class DebouncedOperation
public bool windowOpen;
public bool hasTrailingCallQueued;
public bool isExecuting;
public Task currentTask;
}

private readonly Dictionary<TOperation, DebouncedOperation> operations = new();

protected event Action OnOperationCompleted;
protected event Action<Exception> OnOperationFailed;

protected DebouncedAPI(string service) : base(service) { }

private void OpenWindow(DebouncedOperation op)
Expand All @@ -25,7 +29,7 @@ private void OpenWindow(DebouncedOperation op)
op.windowEndTime = Time.realtimeSinceStartup + Talo.Settings.debounceTimerSeconds;
}

protected void Debounce(TOperation operation)
protected async void Debounce(TOperation operation)
{
if (!operations.ContainsKey(operation))
{
Expand All @@ -41,13 +45,21 @@ protected void Debounce(TOperation operation)
op.isExecuting = true;
OpenWindow(op);

ExecuteDebouncedOperation(operation).ContinueWith((t) => {
op.currentTask = ExecuteDebouncedOperation(operation);

try
{
await op.currentTask;
OnOperationCompleted?.Invoke();
}
catch (Exception ex)
{
OnOperationFailed?.Invoke(ex);
}
finally
{
op.isExecuting = false;
if (t.IsFaulted)
{
Debug.LogError(t.Exception);
}
}, TaskScheduler.FromCurrentSynchronizationContext());
}
}
else
{
Expand Down Expand Up @@ -93,9 +105,16 @@ public async Task ProcessPendingUpdates()
var op = operations[key];
op.hasTrailingCallQueued = false;
op.isExecuting = true;
op.currentTask = ExecuteDebouncedOperation(key);

try
{
await ExecuteDebouncedOperation(key);
await op.currentTask;
OnOperationCompleted?.Invoke();
}
catch (Exception ex)
{
OnOperationFailed?.Invoke(ex);
}
finally
{
Expand All @@ -105,6 +124,53 @@ public async Task ProcessPendingUpdates()
}
}

public async Task<bool> FlushUpdates()
{
bool didWork = false;

var keys = new List<TOperation>(operations.Keys);
foreach (var key in keys)
{
var op = operations[key];

if (op.isExecuting)
{
await op.currentTask;
didWork = true;
}

if (op.hasTrailingCallQueued)
{
while (op.hasTrailingCallQueued)
{
op.hasTrailingCallQueued = false;
op.isExecuting = true;
op.currentTask = ExecuteDebouncedOperation(key);

try
{
await op.currentTask;
OnOperationCompleted?.Invoke();
}
catch (Exception ex)
{
OnOperationFailed?.Invoke(ex);
throw;
}
finally
{
op.isExecuting = false;
op.windowOpen = false;
}

didWork = true;
}
}
}

return didWork;
}

protected abstract Task ExecuteDebouncedOperation(TOperation operation);
}
}
4 changes: 4 additions & 0 deletions Assets/Talo Game Services/Talo/Runtime/APIs/PlayersAPI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,14 @@ public enum DebouncedOperation
public event Action<IdentifyException> OnIdentificationFailed;
public event Action OnIdentityCleared;
public event Action<RejectedProp[]> OnPropsRejected;
public event Action<Player> OnPlayerUpdated;
public event Action<Exception> OnPlayerUpdateFailed;

public PlayersAPI() : base("v1/players")
{
Talo.OnConnectionRestored += OnConnectionRestored;
OnOperationCompleted += () => OnPlayerUpdated?.Invoke(Talo.CurrentPlayer);
OnOperationFailed += (ex) => OnPlayerUpdateFailed?.Invoke(ex);
}

private async void OnConnectionRestored()
Expand Down
21 changes: 14 additions & 7 deletions Assets/Talo Game Services/Talo/Runtime/APIs/SavesAPI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ public enum DebouncedOperation

public event Action<GameSave> OnSaveChosen;
public event Action<GameSave> OnSaveUnloaded;
public event Action<GameSave> OnSaveUpdated;
public event Action<Exception> OnSaveUpdateFailed;

public GameSave[] All
{
Expand All @@ -37,8 +39,13 @@ public GameSave Current
get => savesManager.CurrentSave;
}

private GameSave _lastUpdateResult;

public SavesAPI() : base("v1/game-saves")
{ }
{
OnOperationCompleted += () => OnSaveUpdated?.Invoke(_lastUpdateResult);
OnOperationFailed += (ex) => OnSaveUpdateFailed?.Invoke(ex);
}

internal void Setup()
{
Expand Down Expand Up @@ -196,11 +203,8 @@ protected override async Task ExecuteDebouncedOperation(DebouncedOperation opera
switch (operation)
{
case DebouncedOperation.Update:
var currentSave = savesManager.CurrentSave;
if (currentSave != null)
{
await UpdateSave(currentSave.id);
}
var currentSave = savesManager.CurrentSave ?? throw new Exception("No save is currently loaded");
_lastUpdateResult = await UpdateSave(currentSave.id);
break;
}
}
Expand Down Expand Up @@ -238,7 +242,10 @@ public async Task<GameSave> UpdateSave(int saveId, string newName = "")

if (Talo.IsOffline())
{
if (!string.IsNullOrEmpty(newName)) save.name = newName;
if (!string.IsNullOrEmpty(newName))
{
save.name = newName;
}
save.content = saveContent;
save.updatedAt = DateTime.UtcNow.ToString("O");
}
Expand Down
4 changes: 2 additions & 2 deletions Assets/Talo Game Services/Talo/Runtime/Utils/RequestMock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ private struct RequestHandler
public long status;
}

private static List<RequestHandler> _permanentHandlers = new List<RequestHandler>();
private static List<RequestHandler> _oneTimeHandlers = new List<RequestHandler>();
private static readonly List<RequestHandler> _permanentHandlers = new();
private static readonly List<RequestHandler> _oneTimeHandlers = new();
private static bool _offline;

public static bool Offline
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@ public class SetProp : MonoBehaviour

private void OnEnable()
{
Talo.Players.OnPlayerUpdated += OnPlayerUpdated;
Talo.Players.OnPlayerUpdateFailed += OnPlayerUpdateFailed;
Talo.Players.OnPropsRejected += OnPropsRejected;
}

private void OnDisable()
{
Talo.Players.OnPlayerUpdated -= OnPlayerUpdated;
Talo.Players.OnPlayerUpdateFailed -= OnPlayerUpdateFailed;
Talo.Players.OnPropsRejected -= OnPropsRejected;
}

Expand Down Expand Up @@ -42,6 +46,16 @@ private void UpdateProp()
}
}

private void OnPlayerUpdated(Player player)
{
ResponseMessage.SetText($"{key} saved successfully");
}

private void OnPlayerUpdateFailed(Exception ex)
{
ResponseMessage.SetText($"Save failed: {ex.Message}");
}

private void OnPropsRejected(RejectedProp[] rejectedProps)
{
var reasons = string.Join(", ", Array.ConvertAll(rejectedProps, (rp) => $"[{rp.key}] {rp.message}"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,6 @@ private void Start()
updateSaveButton.clicked += async () =>
{
await Talo.Saves.UpdateCurrentSave();
updateSaveButton.text = "Saved!";
Invoke("ResetUpdateSaveButtonText", 1f);
};

root.Q<Button>("back-btn").clicked += () =>
Expand All @@ -42,6 +40,30 @@ private void Start()
SetupLevelButtons();
}

private void OnEnable()
{
Talo.Saves.OnSaveUpdated += OnSaveUpdated;
Talo.Saves.OnSaveUpdateFailed += OnSaveUpdateFailed;
}

private void OnDisable()
{
Talo.Saves.OnSaveUpdated -= OnSaveUpdated;
Talo.Saves.OnSaveUpdateFailed -= OnSaveUpdateFailed;
}

private void OnSaveUpdated(GameSave save)
{
updateSaveButton.text = "Saved!";
Invoke(nameof(ResetUpdateSaveButtonText), 1f);
}

private void OnSaveUpdateFailed(System.Exception ex)
{
updateSaveButton.text = "Save failed";
Invoke(nameof(ResetUpdateSaveButtonText), 1f);
}

private void SetupLevelButtons()
{
var buttons = new[] { 1, 2, 3 }.Select((level) =>
Expand Down
Loading
Loading