using System.Text.Json; using DeepDrftModels.DTOs; using DeepDrftModels.Enums; using Microsoft.AspNetCore.Components; namespace DeepDrftPublic.Client.Services; /// /// Records share events from SharePopover (Phase 16 §1b / §2.1). After a successful clipboard /// write the popover calls ; this tracker applies the per-(target,channel) /// debounce — at most one event per target+channel per per session — and /// fires the event via navigator.sendBeacon to the proxied api/event/share route. /// /// /// Scoped (per-session) so the debounce memory lives for the session and resets on a fresh load, matching /// the "feels like one act" intent: copying the same link three times in a row is one share, not three. /// The beacon send is fire-and-forget; no anonId is sent in wave 16.1. /// /// public sealed class ShareTracker { // One event per (target, channel) per this window per session (§1b). 60s matches the spec's // recommendation — long enough to fold a flurry of repeat copies into one intent. private static readonly TimeSpan DebounceWindow = TimeSpan.FromSeconds(60); private readonly BeaconInterop _beacon; private readonly string _shareUrl; private readonly Dictionary _lastSent = new(); public ShareTracker(BeaconInterop beacon, NavigationManager navigation) { _beacon = beacon; _shareUrl = $"{navigation.BaseUri}api/event/share"; } /// /// Record a share unless an identical (target, channel) was recorded within the debounce window. /// Returns true when an event was fired, false when debounced — primarily so tests can assert the /// debounce without reaching into the beacon. /// public bool RecordShare(ShareTargetType targetType, string targetKey, ShareChannel channel) => RecordShare(targetType, targetKey, channel, DateTimeOffset.UtcNow); /// /// Debounce-aware record with an injectable so the 60s window is testable /// without wall-clock waits. The parameterless overload above passes . /// public bool RecordShare(ShareTargetType targetType, string targetKey, ShareChannel channel, DateTimeOffset now) { if (string.IsNullOrWhiteSpace(targetKey)) return false; var dedupeKey = $"{targetType}:{targetKey}:{channel}"; if (_lastSent.TryGetValue(dedupeKey, out var last) && now - last < DebounceWindow) return false; _lastSent[dedupeKey] = now; var json = JsonSerializer.Serialize(new ShareEventDto { TargetType = targetType, TargetKey = targetKey, Channel = channel, }); // Fire-and-forget — a dropped share telemetry event is acceptable. _ = _beacon.SendAsync(_shareUrl, json); return true; } }