Editor-side part of SessionServer that manages blob (geometry) synchronization for members. It tracks per-member BlobExchange objects, routes and holds incoming packets that reference geometry, pumps blob exchanges, prunes the BlobStore, reports dropped blob requests back to members, and exposes a host-side BlobNotice and welcome features list.
using System;
using System.Collections.Generic;
using System.Linq;
namespace TeamCreate;
/// <summary>
/// The host's half of mapping-tool geometry synchronization (docs/BLOB-SYNC.md). Each member has one <see cref="BlobExchange"/>: it serves the
/// geometry the member asks for from <see cref="BlobStore"/> and holds the member's edits (and a first subscribe that seeds a scene) until
/// the geometry they name has arrived, so the host never rebroadcasts a state it cannot serve. All members share one pending-bytes budget.
/// </summary>
public sealed partial class SessionServer
{
private readonly Dictionary<Member, BlobExchange> blobMembers = new();
private readonly BlobBudget blobBudget = new();
private DateTime lastBlobPrune = DateTime.UtcNow;
/// <summary>The host's geometry cache. Null disables geometry synchronization and its welcome feature, as a null project root does for files.</summary>
public BlobStore BlobStore { get; set; }
private BlobExchange BlobsOf( Member member )
{
if ( BlobStore == null ) return null;
if ( !blobMembers.TryGetValue( member, out BlobExchange exchange ) )
{
blobMembers[member] = exchange = new BlobExchange( BlobStore, packet => member.Wire.Send( packet ), () => DateTime.UtcNow, blobBudget );
}
return exchange;
}
// The next packet to handle from a member: a released held one first, then the wire. Geometry packets are consumed here, and a packet that
// names missing geometry is held (the member is asked for it on the next pump). Before the handshake nothing is intercepted.
private bool NextPacket( Member member, out Packet packet )
{
BlobExchange exchange = member.Peer == null ? null : BlobsOf( member );
while ( true )
{
if ( exchange != null && exchange.TryTakeReady( out packet ) ) return true;
if ( !member.Wire.Incoming.TryDequeue( out packet ) ) return false;
if ( exchange == null ) return true;
if ( BlobExchange.IsBlobPacket( packet.Kind ) )
{
member.LastSeen = DateTime.UtcNow;
exchange.Handle( packet );
continue;
}
// A subscribe only matters for its objects when it creates the scene; for a scene the host holds they are ignored, so their geometry is not fetched.
if ( packet.Kind == "subscribe" && packet.Scene != null && authority.Scenes.ContainsKey( packet.Scene ) ) return true;
BlobOffer offer = exchange.Offer( packet );
if ( offer == BlobOffer.Process ) return true;
ReportDropped( member, exchange );
}
}
private void ForgetBlobMember( Member member )
{
if ( blobMembers.Remove( member, out BlobExchange exchange ) ) exchange.Release();
}
private void PumpBlobs()
{
foreach ( KeyValuePair<Member, BlobExchange> pair in blobMembers.ToArray() )
{
pair.Value.Pump();
ReportDropped( pair.Key, pair.Value );
}
if ( BlobStore != null && DateTime.UtcNow - lastBlobPrune > TimeSpan.FromMinutes( 5 ) )
{
lastBlobPrune = DateTime.UtcNow;
BlobStore.Prune();
}
}
// A packet the host refused to hold or could not complete is answered so the member is not left waiting: an edit gets the ordinary
// rejection (the member saves its version and takes the host's scene again), a subscribe ends the attempt with the same sentence.
private void ReportDropped( Member member, BlobExchange exchange )
{
foreach ( (Packet Packet, string Reason) item in exchange.TakeDropped() )
{
exchange.TakeNotices();
BlobNotice = item.Reason;
member.Wire.Send( new Packet
{
Kind = "reject",
Id = item.Packet.Id,
Scene = item.Packet.Kind == "subscribe" ? null : item.Packet.Scene,
Error = item.Reason
} );
}
}
/// <summary>The latest geometry refusal on the host's side, for the host's own dock.</summary>
public string BlobNotice { get; private set; }
private List<string> WelcomeFeatures()
{
List<string> features = new List<string>();
if ( !string.IsNullOrEmpty( FileProjectRoot ) ) features.Add( FileSyncLimits.Feature );
if ( BlobStore != null ) features.Add( BlobLimits.Feature );
return features.Count == 0 ? null : features;
}
}