using Fleet.Core;
using Monitor.Protocol;
namespace Monitor.Backend.Application;
/// 一条线路 = 一对(Agent,Probe)。存储里它的序列是 line:<probe_id>,对外的 id 是 <agent_id>~<probe_id>。
public readonly record struct LineKey(NodeId Agent, ProbeId Probe)
{
public string Series => RawChunkHeader.LinePrefix + Probe.Value;
public string Id => Agent.Value + "~" + Probe.Value;
public static bool TryParse(string text, out LineKey key)
{
int split = text.IndexOf('~', StringComparison.Ordinal);
if (split <= 0 || split == text.Length - 1)
{
key = default;
return false;
}
key = new LineKey(new NodeId(text[..split]), new ProbeId(text[(split + 1)..]));
return true;
}
}
/// 一条线路的展示视图:Agent 的状态、Probe 记录,以及这一对现在是否连着。
public sealed record LineView(AgentView Agent, ProbeRecord Probe)
{
public LineKey Key => new(Agent.Agent.Id, new ProbeId(Probe.Id));
public bool Connected => Agent.Probes.Any(p => p.Id == Probe.Id);
public ConnectedProbeInfo? Connection => Agent.Probes.FirstOrDefault(p => p.Id == Probe.Id);
}
/// 线路目录:把 id 解析成(Agent,Probe),列出所有线路。
public sealed class LineService
{
private readonly AgentService _agents;
private readonly IProbeStore _probes;
private readonly LinkService _links;
public LineService(AgentService agents, IProbeStore probes, LinkService links)
{
_links = links;
_agents = agents;
_probes = probes;
}
/// 所有线路:每个 Agent 和每个未吊销的 Probe 配成一对,只留下线路开着的(见 );关掉的线路历史数据还在,按 id 仍然查得到。
public async ValueTask> ListAsync(Func include, CancellationToken ct)
{
IReadOnlyList agents = await _agents.ListAsync(ct);
IReadOnlyList probes = [.. (await _probes.ListAsync(ct)).Where(p => p.RevokedAtMs is null)];
LinkSnapshot links = await _links.SnapshotAsync(ct);
return [.. agents.Where(include).SelectMany(a => probes.Where(p => links.IsLinked(p.Id, a.Agent.Id.Value)).Select(p => new LineView(a, p)))];
}
///
/// 线路 id 是 agent~probe。只给 Agent id 时:恰好有一个未吊销的 Probe 就用它,否则要求明确指定(probe_required)。
///
public async ValueTask> ResolveAsync(string id, CancellationToken ct)
{
string agentPart = id;
string? probePart = null;
if (id.Contains('~', StringComparison.Ordinal))
{
if (!LineKey.TryParse(id, out LineKey key))
{
return Result.Fail(MonitorErrors.AgentNotFound, "line not found");
}
agentPart = key.Agent.Value;
probePart = key.Probe.Value;
}
AgentView? agent = await _agents.GetAsync(new NodeId(agentPart), ct);
if (agent is null)
{
return Result.Fail(MonitorErrors.AgentNotFound, "line not found");
}
IReadOnlyList probes = await _probes.ListAsync(ct);
ProbeRecord? probe;
if (probePart is not null)
{
probe = probes.FirstOrDefault(p => p.Id == probePart);
if (probe is null)
{
return Result.Fail(MonitorErrors.ProbeNotFound, "probe not found");
}
}
else
{
ProbeRecord[] active = [.. probes.Where(p => p.RevokedAtMs is null)];
if (active.Length != 1)
{
return Result.Fail(MonitorErrors.ProbeRequired, active.Length == 0 ? "there is no active probe" : "several probes exist: use ~ as the line id");
}
probe = active[0];
}
return Result.Ok(new LineView(agent, probe));
}
}