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
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,11 @@ public class JsPuzzleChange
/// </summary>
public string channel { get; set; }
}

public class JsPuzzleReset
{
public string[] puzzleIds { get; set; }
public string channel { get; set; }
}
#pragma warning restore IDE1006 // Naming Styles
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ else
puzzleFrame.contentWindow.postMessage({type: "puzzlesynced", changes: changes}, "*");
}

async function onPuzzleResetSynced (reset) {
puzzleFrame.contentWindow.postMessage({type: "puzzleresetsynced", reset: reset}, "*");
}

async function onSetCoopMode(mode) {
puzzleFrame.contentWindow.postMessage({ type: "setCoopMode", mode: mode }, "*");
}
Expand All @@ -79,6 +83,9 @@ else
else if (ev.data.type === "puzzlechanged") {
await hostComponent.invokeMethodAsync("OnPuzzleChangedAsync", ev.data.changes);
}
else if (ev.data.type === "puzzlereset") {
await hostComponent.invokeMethodAsync("OnPuzzleResetAsync", ev.data.changes);
}
});

</script>
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,6 @@ public partial class ClientSyncComponent

TableClient TableClient { get; set; }

List<string> ReceivedValues { get; set; } = new List<string>();

Timer Timer { get; set; }

bool Paused { get; set; } = false;
Expand Down Expand Up @@ -182,7 +180,23 @@ public async void OnPuzzleChangedAsync(JsPuzzleChange[] puzzleChanges)
value: change.value,
channel: change.channel);

await TableClient.UpsertEntityAsync(puzzleEntry);
await TableClient.UpsertEntityAsync(puzzleEntry, TableUpdateMode.Replace);
}
}

[JSInvokable]
public async Task OnPuzzleResetAsync(JsPuzzleReset resets)
{
if (!SyncEnabled || Paused)
{
return;
}

// Create a reset entry for each sub-puzzle
foreach (string subPuzzleId in resets.puzzleIds)
{
PuzzleItemProperty resetEntry = PuzzleItemProperty.CreateReset(PuzzleId, TeamId, Base64UrlTextEncoder.Encode(Encoding.UTF8.GetBytes(subPuzzleId)), PuzzleUserId, channel: resets.channel);
await TableClient.UpsertEntityAsync(resetEntry, TableUpdateMode.Replace);
}
}

Expand Down Expand Up @@ -229,29 +243,77 @@ private async Task SyncAsync()
{
bool foundNewData = false;

var newChanges = TableClient.QueryAsync<PuzzleItemProperty>(entry => entry.PartitionKey == PuzzleItemProperty.CreatePartitionKey(PuzzleId, TeamId) && entry.Timestamp > LastSyncUtc);
var unsortedChanges = TableClient.QueryAsync<PuzzleItemProperty>(entry => entry.PartitionKey == PuzzleItemProperty.CreatePartitionKey(PuzzleId, TeamId) && entry.Timestamp > LastSyncUtc);

List<JsPuzzleChange> jsChanges = new List<JsPuzzleChange>();
await foreach (PuzzleItemProperty entry in newChanges)
List<PuzzleItemProperty> newChanges = new List<PuzzleItemProperty>();
await foreach (PuzzleItemProperty entry in unsortedChanges)
{
newChanges.Add(entry);
}
newChanges.Sort((a, b) => a.Timestamp?.CompareTo(b.Timestamp!.Value) ?? 0);

// If a puzzle was reset, remove the earlier data for that puzzle to avoid it flashing in and then disappearing

bool removedEntries = false;
do
{
removedEntries = false;
for (int i = 0; i < newChanges.Count; i++)
{
PuzzleItemProperty entry = newChanges[i];
if (entry.IsReset)
{
// Remove all data entries for this sub-puzzle
int removedForReset = newChanges.RemoveAll(e => e.SubPuzzleId == entry.SubPuzzleId &&
e.Timestamp < entry.Timestamp && !e.IsReset);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you do not need to do the IsReset check here - if there are two resets for the same puzzle you only need one and your timestamp check will keep you from removing yourself

if (removedForReset > 0)
{
removedEntries = true;
Copy link
Contributor

@tabascq tabascq Sep 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just say i -= removedForReset; and remove the removedEntries bool and the do loop

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussion: Do one loop to identify reset timestamps and then one to build the collection of post-reset data to populate onto the page to guarantee O(N)

break;
}
}
}
} while (removedEntries);

List < JsPuzzleChange > jsChanges = new List<JsPuzzleChange>();
foreach (PuzzleItemProperty entry in newChanges)
{
foundNewData = true;
ReceivedValues.Add(entry.Value);
jsChanges.Add(new JsPuzzleChange()
if (entry.IsReset)
{
// If this is a reset, we need to send a reset message to the JS side
await JSRuntime.InvokeVoidAsync("onPuzzleResetSynced", new JsPuzzleReset()
{
puzzleIds = new[] { Encoding.UTF8.GetString(Base64UrlTextEncoder.Decode(entry.SubPuzzleId)) },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not build a list of IDs and send them all at once

channel = entry.Channel
});
}
else
{
locationKey = entry.LocationKey,
playerId = PuzzleUserId.ToString(),
propertyKey = entry.PropertyKey,
puzzleId = Encoding.UTF8.GetString(Base64UrlTextEncoder.Decode(entry.SubPuzzleId)),
teamId = TeamId.ToString(),
value = entry.Value
});
jsChanges.Add(new JsPuzzleChange()
{
locationKey = entry.LocationKey,
playerId = PuzzleUserId.ToString(),
propertyKey = entry.PropertyKey,
puzzleId = Encoding.UTF8.GetString(Base64UrlTextEncoder.Decode(entry.SubPuzzleId)),
teamId = TeamId.ToString(),
value = entry.Value,
channel = entry.Channel
});

//morganb debug
await JSRuntime.InvokeVoidAsync("onPuzzleSynced", [jsChanges.ToArray()]);
jsChanges.Clear();
}

LastSyncUtc = entry.Timestamp > LastSyncUtc ? entry.Timestamp.Value : LastSyncUtc;
DisplayLastSyncUtc = entry.Timestamp > DisplayLastSyncUtc ? entry.Timestamp.Value : DisplayLastSyncUtc;
}

if (foundNewData)
{
await JSRuntime.InvokeVoidAsync("onPuzzleSynced", [jsChanges.ToArray()]);
// morganb debug
//await JSRuntime.InvokeVoidAsync("onPuzzleSynced", [jsChanges.ToArray()]);
await InvokeAsync(StateHasChanged);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public class PuzzleItemProperty : ITableEntity
public PuzzleItemProperty()
{
}

// todo morganb: translate from string player name to playerId
public PuzzleItemProperty(int puzzleId, int teamId, int playerId, string subPuzzleId, string locationKey, string propertyKey, string value, string channel)
{
Expand All @@ -28,6 +28,14 @@ public PuzzleItemProperty(int puzzleId, int teamId, int playerId, string subPuzz
Channel = channel;
}

public static PuzzleItemProperty CreateReset(int puzzleId, int teamId, string subPuzzleId, int playerId, string channel)
{
return new PuzzleItemProperty(puzzleId, teamId, playerId, subPuzzleId, String.Empty, String.Empty, String.Empty, channel)
{
IsReset = true
};
}

private static string CreateRowKey(string subPuzzleId, string locationKey, string propertyKey)
{
return $"{subPuzzleId}|{propertyKey}|{locationKey}";
Expand All @@ -49,6 +57,7 @@ public static string CreatePartitionKey(int puzzleId, int teamId)
/// TODO: Implement channels beyond having the property
/// </summary>
public string Channel { get; set; }
public bool IsReset { get; set; } = false;

// Set automatically by the Table service
public DateTimeOffset? Timestamp { get; set; }
Expand Down