File size: 5,019 Bytes
ca978e6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
/**
 * Bayan — Analysis Controller (Phase 7.1 Simplified)
 *
 * Responsibilities (ONLY):
 *   - Adaptive debounce (2-tier: normal / large text)
 *   - Stale request cancellation
 *   - Text validation
 *   - Message passing to background.js
 *
 * Does NOT own:
 *   - Caching (background.js)
 *   - Retry logic (background.js)
 *   - Timeout (background.js)
 *   - Network (background.js)
 *
 * Requires: BAYAN (shared/constants.js)
 */

// eslint-disable-next-line no-unused-vars
const BayanController = (() => {
  'use strict';

  // ── Debounce config ──
  const DEBOUNCE_NORMAL = 500;
  const DEBOUNCE_LARGE = 1000;
  const LARGE_TEXT_THRESHOLD = 2000;

  // ── State ──
  let _debounceTimer = null;
  let _inflight = false;

  // ── Protected site check ──
  const _isProtected = BAYAN.PROTECTED_HOSTS.includes(window.location.hostname);

  // ══════════════════════════════════════════════════════════
  // Text validation
  // ══════════════════════════════════════════════════════════

  function hasArabic(text) {
    if (!text || text.length < BAYAN.MIN_TEXT_LENGTH) return false;
    const arabicChars = (text.match(/[\u0600-\u06FF]/g) || []).length;
    return arabicChars >= BAYAN.MIN_ARABIC_CHARS;
  }

  function validateText(text) {
    if (!text) return { valid: false, reason: 'empty' };
    if (text.length < BAYAN.MIN_TEXT_LENGTH) return { valid: false, reason: 'too_short' };
    if (text.length > BAYAN.MAX_TEXT_LENGTH) return { valid: false, reason: 'too_long' };
    if (!hasArabic(text)) return { valid: false, reason: 'no_arabic' };
    return { valid: true };
  }

  // ══════════════════════════════════════════════════════════
  // Core: debounced analysis via background bridge
  // ══════════════════════════════════════════════════════════

  /**
   * Schedule a debounced analysis.
   * Background.js owns caching, retry, and timeout.
   *
   * @param {string} text
   * @param {function(Object|null): void} onResult
   * @param {function(): string} getCurrentText — staleness checker
   */
  function scheduleAnalysis(text, onResult, getCurrentText) {
    if (_debounceTimer) {
      clearTimeout(_debounceTimer);
      _debounceTimer = null;
    }

    if (!validateText(text).valid) {
      onResult(null);
      return;
    }

    const delay = text.length > LARGE_TEXT_THRESHOLD ? DEBOUNCE_LARGE : DEBOUNCE_NORMAL;

    _debounceTimer = setTimeout(() => {
      _debounceTimer = null;

      // Staleness check: text changed during debounce
      if (getCurrentText() !== text) return;

      executeAnalysis(text, onResult, getCurrentText);
    }, delay);
  }

  /**
   * Send analysis request to background.js.
   * No timeout, no retry, no cache — background handles all of that.
   */
  function executeAnalysis(text, onResult, getCurrentText) {
    _inflight = true;

    chrome.runtime.sendMessage({ type: 'INLINE_ANALYZE', text }, (response) => {
      _inflight = false;

      // Extension context invalidated (e.g., extension reloaded)
      if (chrome.runtime.lastError) {
        console.warn('[Bayan Controller]', chrome.runtime.lastError.message);
        onResult(null);
        return;
      }

      // Staleness check: text changed during fetch
      if (getCurrentText() !== text) return;

      if (!response || response.error) {
        onResult(null);
        return;
      }

      onResult(response.data);
    });
  }

  // ══════════════════════════════════════════════════════════
  // Cancellation
  // ══════════════════════════════════════════════════════════

  function cancelAll() {
    if (_debounceTimer) {
      clearTimeout(_debounceTimer);
      _debounceTimer = null;
    }
    _inflight = false;
  }

  function destroy() {
    cancelAll();
  }

  // ══════════════════════════════════════════════════════════
  // Public API
  // ══════════════════════════════════════════════════════════

  return {
    scheduleAnalysis,
    cancelAll,
    destroy,
    validateText,
    hasArabic,
    isProtectedSite() { return _isProtected; },
    isInFlight() { return _inflight; },
  };
})();