Cysharp / MagicOnion

Unified Realtime/API framework for .NET platform and Unity.
MIT License
3.68k stars 417 forks source link

Change the backend of HubGroup to Multicaster #778

Closed mayuki closed 1 month ago

mayuki commented 1 month ago

This PR migrates the implementation of StreamingHub's Group to a new library called Multicaster. This allows for controls of the Group to be executed within the Hub's methods or within any application logic.

Added APIs

Client Property

A property Client that returns the receiver for the currently connected client is added to StreamingHubBase<THub, TReceiver>. This eliminates the need to create a group for receiver invocations to a single client.

Client.OnMessage("Sender", "Hello from the server!");

Updated APIs

IGroup -> IGroup

Group.AddAsync in StreamingHubBase<THub, TReceiver> now returns an IGroup<TReceiver>.

// Before
IGroup group = await Group.AddAsync("ChatRoom-A");
// After
IGroup<IChatReceiver> group = await Group.AddAsync("ChatRoom-A");

Changes to Broadcast Methods

StreamingHub.Broadcast* and IGroup.CreateBroadcaster* methods have been updated to new APIs via IMulticastGroup.

// Before
var group = await Group.AddAsync("ChatRoom-A");
BroadcastToSelf(group).OnMessage("Sender", "Message to the current client");
Broadcast(group).OnMessage("Sender", "Message to clients");
BroadcastExcept(group, otherClientContextId).OnMessage("Sender", "Message to clients excepts specified clients");
BroadcastTo(group, otherClientContextId).OnMessage("Sender", "Message to specified clients");
// After
var group = await Group.AddAsync("ChatRoom-A");
Client.OnMessage("Sender", "Message to the current client"); // or `group.Only(Context.ContextId)`
group.All.OnMessage("Sender", "Message to clients");
group.Except(otherClientContextId).OnMessage("Sender", "Message to clients except specified clients");
group.Only(otherClientContextId).OnMessage("Sender", "Message to specified clients");

Changes to Types Specified in GroupConfigurationAttribute

Although GroupConfigurationAttribute can be used to select a group's implementation for each Hub, IHubGroupRepositoryFactory has been removed. Instead, a type for IMulticastGroupProvider must be specified.

[GroupConfiguration(typeof(Cysharp.Runtime.Multicast.Distributed.RedisGroupProvider))]
public class MyHub : StreamingHubBase<...> { ... }

Controlling Groups through Application Logic

By obtaining IMulticastGroupProvider through DI, you can manage the group's lifecycle and members within the application logic.

// for internal API services.
class InternalBattleService(BattleFieldRepository battleFields, IMulticastGroupProvider groupProvider) : ServiceBase<IInternalBattleService>
{
    public async UnaryResult CreateBattleAsync(string battleId)
    {
        var group = groupProvider.GetOrAddSynchronous<UserId, IBattleFieldHubReceiver>(battleId);
        battleFields.Battles[battleId] = new BattleField(group);
    }
    public async UnaryResult CompleteBattleAsync(string battleId)
    {
        if (battleFields.Battles.TryRemove(battleId, out var battleField))
        {
            battleField.Dispose();
        }
    }
}

// for Client
class BattleFieldHub(BattleFieldRepository battleFields) : StreamingHubBase<IBattleFieldHub, IBattleFieldHubReceiver>
{
    public async Task JoinAsync(string battleId)
    {
        battleFields.Battles[battleId].AddMember(User.Id, Client);
    }
    protected override ValueTask OnDisconnected()
    {
        battleFields.Battles[battleId].RemoveMember(User.Id);
        return default;
    }
}

// Game Logics / Models
class BattleFieldRepository
{
    public ConcurrentDictionary<string, BattleField> Battles { get; } = new();
}
class BattleField : IDisposable
{
    private readonly IMulticastSyncGroup<UserId, IBattleHubReceiver> _group;
    public BattleField(IMulticastSyncGroup<UserId, IBattleHubReceiver> group)
    {
        _group = group;
    }
    public void AddMember(UserId id, IBattleHubReceiver member) => _group.Add(id, member);
    public void RemoveMember(UserId id) => _group.Remove(id);
    public void Dispose() => _group.Dispose(); // Unregister the group from the group provider.
}

Groups created with IMulticastGroupProvider can broadcast to clients through the group by registering StreamingHub's Client property.

While this provides flexible management of groups, be aware that the client registration and group lifecycle are no longer managed by MagicOnion, and manual management will be necessary.

Migration v6 -> v7

The following Shims can be imported into a project to migrate from v6 to v7 to maintain compatibility where existing APIs are used. You can import this Shim into your project and use StreamingHubBaseCompat instead of StreamingHubBase.

https://gist.github.com/mayuki/974ab44d5464eefb821a5619209f7068