#if UGX_BRIDGE || UNITY_EDITOR
using System;
using System.Collections.Generic;
using Uigenx.Binding;
using Uigenx.Protocol;
using UnityEngine;
namespace Uigenx.Bridge
{
///
/// Serves `data.subscribe`, `data.unsubscribe` and `data.set`. Each session samples only the
/// paths it asked for, at its own rate, and only changed values leave the process.
///
public sealed class UgxDataStreamHandler : IDisposable
{
/// Upper bound on live sessions. The studio opens one per inspector.
public const int MaxSessions = 8;
/// Upper bound on watched paths per session.
public const int MaxPathsPerSession = 512;
readonly UgxBridgeClient _client;
readonly Func _sourceProvider;
readonly Dictionary _sessions = new Dictionary(StringComparer.Ordinal);
public UgxDataStreamHandler(UgxBridgeClient client, Func sourceProvider)
{
_client = client ?? throw new ArgumentNullException(nameof(client));
_sourceProvider = sourceProvider ?? throw new ArgumentNullException(nameof(sourceProvider));
client.Register(UgxMessageTypes.DataSubscribe, OnSubscribe);
client.Register(UgxMessageTypes.DataUnsubscribe, OnUnsubscribe);
client.Register(UgxMessageTypes.DataSet, OnSet);
}
public int SessionCount => _sessions.Count;
public void Dispose()
{
_client.Unregister(UgxMessageTypes.DataSubscribe);
_client.Unregister(UgxMessageTypes.DataUnsubscribe);
_client.Unregister(UgxMessageTypes.DataSet);
foreach (var session in _sessions.Values) _client.ClearDeltas(session.Id);
_sessions.Clear();
}
/// Main thread. Samples every due session and flushes the coalesced deltas.
public void Tick(double now)
{
if (_sessions.Count == 0) return;
var source = _sourceProvider();
if (source == null) return;
bool queued = false;
foreach (var session in _sessions.Values)
{
if (now < session.NextSampleTime) continue;
session.NextSampleTime = now + session.Interval;
queued |= session.Sample(source, _client);
}
if (queued) _client.FlushDeltas();
}
void OnSubscribe(UgxEnvelope envelope)
{
var body = envelope.BodyAs();
if (string.IsNullOrEmpty(body.Session))
{
_client.SendError(UgxErrorCodes.BadRequest, "data.subscribe needs a session", envelope.Id);
return;
}
if (!_sessions.ContainsKey(body.Session) && _sessions.Count >= MaxSessions)
{
_client.SendError(UgxErrorCodes.BadRequest,
"too many live data sessions, the limit is " + MaxSessions, envelope.Id);
return;
}
if (body.Paths == null) body.Paths = new List();
if (body.Paths.Count > MaxPathsPerSession)
{
_client.SendError(UgxErrorCodes.BadRequest,
"a session may watch at most " + MaxPathsPerSession + " paths", envelope.Id);
return;
}
var session = new Session(body.Session, body.Paths, body.Hz);
_sessions[body.Session] = session;
var source = _sourceProvider();
var snapshot = new UgxDataSnapshot { Session = session.Id, Seq = 0 };
if (source != null)
{
foreach (var path in session.Paths)
{
if (!source.TryGet(path, out var value)) continue;
snapshot.Values[path] = value;
session.Remember(path, value);
}
}
_client.Send(UgxMessageTypes.DataSnapshot, snapshot, re: envelope.Id);
}
void OnUnsubscribe(UgxEnvelope envelope)
{
var body = envelope.BodyAs();
if (string.IsNullOrEmpty(body.Session)) return;
_sessions.Remove(body.Session);
_client.ClearDeltas(body.Session);
}
void OnSet(UgxEnvelope envelope)
{
var body = envelope.BodyAs();
var source = _sourceProvider();
if (source == null)
{
_client.SendError(UgxErrorCodes.NotPlaying, "no data source is active", envelope.Id);
return;
}
if (!source.TrySet(body.Path, body.Value))
_client.SendError(UgxErrorCodes.BadRequest,
"cannot write '" + body.Path + "' on the active data source", envelope.Id);
}
sealed class Session
{
public readonly string Id;
public readonly List Paths;
public readonly double Interval;
public double NextSampleTime;
readonly Dictionary _last;
public Session(string id, List paths, double hz)
{
Id = id;
Paths = paths ?? new List();
Interval = hz > 0d ? 1d / hz : 1d / UgxSettings.DefaultDataStreamHz;
_last = new Dictionary(Paths.Count, StringComparer.Ordinal);
}
public void Remember(string path, object value) => _last[path] = value;
/// Returns true when at least one change was queued.
public bool Sample(IUgxDataSource source, UgxBridgeClient client)
{
bool any = false;
for (int i = 0; i < Paths.Count; i++)
{
var path = Paths[i];
if (!source.TryGet(path, out var value)) continue;
if (_last.TryGetValue(path, out var previous) && Equals(previous, value)) continue;
_last[path] = value;
client.QueueDelta(Id, path, value);
any = true;
}
return any;
}
}
}
}
#endif