using System.Text.Json; using System.Text.Json.Serialization; using DeepDrftModels.DTOs; using DeepDrftModels.Enums; using Microsoft.AspNetCore.Components; namespace DeepDrftPublic.Client.Services; /// /// Production (Phase 16 §2.2; telemetry transport-resilience). Serializes the /// play classification once and dispatches it down the arm the close chose: /// /// posts via the first-party — a same-origin /// HttpClient fetch to api/event/play, used for the normal close paths (organic end / /// track-switch / stop) that a privacy-hardened browser would block if they used a name-matched /// sendBeacon module. /// fires (sendBeacon) for the /// tab-unload edge, where an awaited fetch would be cancelled. /// /// Both arms send byte-identical payloads (same DTO, same anonId, same JSON options). The current /// anonId (wave 16.3) is read synchronously from the warmed cache and /// omitted when null (storage unavailable / not yet warmed) — an anonId-less play still counts, it just /// doesn't contribute to the listener tally. /// public sealed class BeaconPlayEventSink : IPlayEventSink { // Omit a null anonId from the wire payload (§2.2 — "omitted entirely" when absent) rather than // sending "anonId":null. The API treats absent and null identically, so this is cosmetic minimalism; // it does not change the integer enum encoding the 16.1 contract already relies on. private static readonly JsonSerializerOptions EventJson = new() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; private readonly IEventPoster _poster; private readonly BeaconInterop _beacon; private readonly IAnonIdProvider _anonId; private readonly string _playUrl; public BeaconPlayEventSink( IEventPoster poster, BeaconInterop beacon, IAnonIdProvider anonId, NavigationManager navigation) { _poster = poster; _beacon = beacon; _anonId = anonId; // The WASM client posts to its own host, which proxies to DeepDrftAPI. BaseUri carries a // trailing slash; the route does not lead with one. _playUrl = $"{navigation.BaseUri}api/event/play"; } public Task EmitPlayAsync(string trackEntryKey, PlayBucket bucket) => _poster.PostAsync(_playUrl, Serialize(trackEntryKey, bucket)); public void EmitPlayOnUnload(string trackEntryKey, PlayBucket bucket) // Fire-and-forget: the beacon survives unload; the C# task may not, and we do not act on the // result either way. => _ = _beacon.SendAsync(_playUrl, Serialize(trackEntryKey, bucket)); private string Serialize(string trackEntryKey, PlayBucket bucket) => JsonSerializer.Serialize(new PlayEventDto { TrackEntryKey = trackEntryKey, Bucket = bucket, AnonId = _anonId.Current, }, EventJson); }