1package messaging
2
3import (
4 "crypto/sha256"
5 "fmt"
6 "log"
7 "sync"
8 "time"
9)
10
11type RequestFingerprint struct {
12 URL string
13 WebViewID string
14 Timestamp int64
15 Hash string
16}
17
18type PaneRequestDeduplicator struct {
19 mu sync.RWMutex
20 recentRequests map[string]*RequestFingerprint
21 requestIDs map[string]bool
22 debounceWindow time.Duration
23 cleanupInterval time.Duration
24 lastCleanup time.Time
25}
26
27func NewPaneRequestDeduplicator() *PaneRequestDeduplicator {
28 return &PaneRequestDeduplicator{
29 recentRequests: make(map[string]*RequestFingerprint),
30 requestIDs: make(map[string]bool),
31 debounceWindow: 200 * time.Millisecond,
32 cleanupInterval: 5 * time.Second,
33 lastCleanup: time.Now(),
34 }
35}
36
37func (d *PaneRequestDeduplicator) generateFingerprint(intent *WindowIntent, webViewID string) string {
38 normalizedTime := intent.Timestamp / 100
39 data := fmt.Sprintf("%s:%s:%d", intent.URL, webViewID, normalizedTime)
40 hash := sha256.Sum256([]byte(data))
41 return fmt.Sprintf("%x", hash[:8])
42}
43
44func (d *PaneRequestDeduplicator) IsDuplicate(intent *WindowIntent, webViewID string) (bool, string) {
45 d.mu.Lock()
46 defer d.mu.Unlock()
47
48 if time.Since(d.lastCleanup) > d.cleanupInterval {
49 d.cleanup()
50 }
51
52 if intent.RequestID != "" {
53 if d.requestIDs[intent.RequestID] {
54 return true, fmt.Sprintf("duplicate request ID: %s", intent.RequestID)
55 }
56 }
57
58 fingerprint := d.generateFingerprint(intent, webViewID)
59
60 if existing, exists := d.recentRequests[fingerprint]; exists {
61 timeDiff := time.Duration(intent.Timestamp-existing.Timestamp) * time.Millisecond
62 if timeDiff < d.debounceWindow {
63 return true, fmt.Sprintf("duplicate content fingerprint: %s (within %v)", fingerprint, timeDiff)
64 }
65 }
66
67 d.recentRequests[fingerprint] = &RequestFingerprint{
68 URL: intent.URL,
69 WebViewID: webViewID,
70 Timestamp: intent.Timestamp,
71 Hash: fingerprint,
72 }
73
74 if intent.RequestID != "" {
75 d.requestIDs[intent.RequestID] = true
76 }
77
78 return false, ""
79}
80
81func (d *PaneRequestDeduplicator) cleanup() {
82 now := time.Now().UnixMilli()
83 cutoff := now - int64(d.debounceWindow*3/time.Millisecond)
84
85 for key, req := range d.recentRequests {
86 if now-req.Timestamp > cutoff {
87 delete(d.recentRequests, key)
88 }
89 }
90
91 if len(d.requestIDs) > 100 {
92 d.requestIDs = make(map[string]bool)
93 }
94
95 d.lastCleanup = time.Now()
96 log.Printf("[deduplicator] Cleanup completed: %d fingerprints, %d request IDs", len(d.recentRequests), len(d.requestIDs))
97}
98
99// ClearRequestID removes a specific request ID from the deduplication cache
100// This allows the same RequestID to be used again (e.g., for OAuth verification -> real popup flow)
101func (d *PaneRequestDeduplicator) ClearRequestID(requestID string) {
102 if requestID == "" {
103 return
104 }
105
106 d.mu.Lock()
107 defer d.mu.Unlock()
108
109 if d.requestIDs[requestID] {
110 delete(d.requestIDs, requestID)
111 log.Printf("[deduplicator] Cleared request ID: %s", requestID)
112 }
113}