Skip to content

Commit 0e7e51f

Browse files
committed
refactor: extract debounce core; move compatibility wrapper factory to word-mode; wire compat wrapper in utils
1 parent 9c3f2cf commit 0e7e51f

File tree

3 files changed

+172
-132
lines changed

3 files changed

+172
-132
lines changed

src/debounce.js

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// Debounce utilities separated from utils.js to avoid mixing domain logic.
2+
export const DEFAULT_DEBOUNCE_DELAY = 5000;
3+
4+
export function debounce(func, delay) {
5+
const normalisedDelay =
6+
Number.isFinite(delay) && delay >= 0 ? delay : DEFAULT_DEBOUNCE_DELAY;
7+
8+
if (normalisedDelay === 0) {
9+
return function (...args) {
10+
return func.apply(this, args);
11+
};
12+
}
13+
14+
let timeoutId;
15+
return function (...args) {
16+
if (timeoutId) {
17+
clearTimeout(timeoutId);
18+
}
19+
timeoutId = setTimeout(() => func.apply(this, args), normalisedDelay);
20+
};
21+
}
22+
23+
// Value shape: { fn: Function, timeoutId: number|null }
24+
let debouncedCapitalizationMap = new WeakMap();
25+
26+
export function __resetDebouncedMapForTests() {
27+
debouncedCapitalizationMap = new WeakMap();
28+
}
29+
30+
export function clearDebouncedCapitalisationCache() {
31+
debouncedCapitalizationMap = new WeakMap();
32+
}
33+
34+
export function flushAndClearDebouncedCapitalisations() {
35+
try {
36+
debouncedCapitalizationMap.forEach?.(() => {});
37+
} catch {
38+
/* ignore */
39+
}
40+
}
41+
42+
export function cancelDebouncedForElement(element) {
43+
if (!element || typeof element !== 'object') return;
44+
try {
45+
const entry = debouncedCapitalizationMap.get(element);
46+
if (entry && entry.timeoutId) {
47+
clearTimeout(entry.timeoutId);
48+
entry.timeoutId = null;
49+
}
50+
} catch (e) {
51+
console.debug(
52+
'WeakMap access error (extension may be reloading):',
53+
e.message
54+
);
55+
}
56+
}
57+
58+
// getDebouncedCapitaliseText: returns a wrapped debounced function for an element
59+
// capitaliserFn must be provided by the caller (keeps this module independent)
60+
export function getDebouncedCapitaliseText(
61+
element,
62+
delay = DEFAULT_DEBOUNCE_DELAY,
63+
capitaliserFn
64+
) {
65+
if (!element || typeof element !== 'object') return () => {};
66+
67+
const existing = debouncedCapitalizationMap.get(element);
68+
if (existing && typeof existing.fn === 'function') return existing.fn;
69+
70+
let timeoutId = null;
71+
const wrapped = function (targetElement) {
72+
if (!targetElement || typeof targetElement !== 'object') return;
73+
if (timeoutId) clearTimeout(timeoutId);
74+
if (!Number.isFinite(delay) || delay < 0) delay = DEFAULT_DEBOUNCE_DELAY;
75+
if (delay === 0) {
76+
if (typeof capitaliserFn === 'function') {
77+
capitaliserFn(targetElement);
78+
}
79+
return;
80+
}
81+
timeoutId = setTimeout(() => {
82+
timeoutId = null;
83+
try {
84+
if (typeof capitaliserFn === 'function') {
85+
capitaliserFn(targetElement);
86+
}
87+
} catch {
88+
/* ignore */
89+
}
90+
}, delay);
91+
try {
92+
debouncedCapitalizationMap.set(element, { fn: wrapped, timeoutId });
93+
} catch (e) {
94+
console.debug(
95+
'WeakMap set error (extension may be reloading):',
96+
e.message
97+
);
98+
}
99+
};
100+
101+
try {
102+
debouncedCapitalizationMap.set(element, { fn: wrapped, timeoutId });
103+
} catch (e) {
104+
console.debug('WeakMap set error (extension may be reloading):', e.message);
105+
}
106+
return wrapped;
107+
}

src/utils.js

Lines changed: 28 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,16 @@ import {
3030
stringToKeyValuePairsCore,
3131
arrayToMapCore,
3232
} from './word-mode';
33+
import { createGetDebouncedCapitaliseText } from './word-mode';
3334
import { enableSentenceMode, disableSentenceMode } from './sentence-mode';
35+
import {
36+
DEFAULT_DEBOUNCE_DELAY as _DEFAULT_DEBOUNCE_DELAY,
37+
debounce as _debounce,
38+
__resetDebouncedMapForTests as _resetDebouncedMapForTests,
39+
clearDebouncedCapitalisationCache as _clearDebouncedCapitalisationCache,
40+
flushAndClearDebouncedCapitalisations as _flushAndClearDebouncedCapitalisations,
41+
cancelDebouncedForElement as _cancelDebouncedForElement,
42+
} from './debounce';
3443
// Re-export commonly used option and key names so tests can reliably import them from a single module.
3544
export {
3645
shouldCapitaliseI,
@@ -792,139 +801,32 @@ export function arrayToMap(obj) {
792801
return {};
793802
}
794803

795-
// Debounce function for sliding window delay
796-
export const DEFAULT_DEBOUNCE_DELAY = 5000;
797-
798-
export function debounce(func, delay) {
799-
// Normalise delay: fall back to DEFAULT_DEBOUNCE_DELAY for invalid values (NaN, negative, null, undefined)
800-
const normalisedDelay =
801-
Number.isFinite(delay) && delay >= 0 ? delay : DEFAULT_DEBOUNCE_DELAY;
802-
803-
// Special case: a zero delay should execute immediately (synchronously) as per test expectations
804-
if (normalisedDelay === 0) {
805-
return function (...args) {
806-
return func.apply(this, args);
807-
};
808-
}
809-
810-
let timeoutId;
811-
return function (...args) {
812-
if (timeoutId) {
813-
clearTimeout(timeoutId); // sliding window behaviour
814-
}
815-
timeoutId = setTimeout(() => func.apply(this, args), normalisedDelay);
816-
};
817-
}
804+
// Re-export debounce helpers from dedicated module
805+
export const DEFAULT_DEBOUNCE_DELAY = _DEFAULT_DEBOUNCE_DELAY;
806+
export const debounce = _debounce;
818807

819808
// Helper to expose current sentence case mode without leaking internal dictionary object
820809
export function isSentenceCaseEnabled() {
821810
return !!optionsDictionary[shouldConvertToSentenceCase];
822811
}
823812

824-
// Value shape: { fn: Function, timeoutId: number|null }
825-
let debouncedCapitalizationMap = new WeakMap();
826-
827813
// TEST-ONLY helper (safe in prod; no reference leakage) to clear debounced map
828-
export function __resetDebouncedMapForTests() {
829-
debouncedCapitalizationMap = new WeakMap();
830-
}
831-
832-
// Public helper to clear per-element debounced functions (used when switching modes)
833-
export function clearDebouncedCapitalisationCache() {
834-
// Cancel any outstanding timers without flushing
835-
debouncedCapitalizationMap = new WeakMap();
836-
}
837-
838-
// Force flush (run immediately) all pending debounced capitalisations then clear cache.
839-
export function flushAndClearDebouncedCapitalisations() {
840-
try {
841-
debouncedCapitalizationMap.forEach?.(() => {}); // no-op safeguard for older environments
842-
} catch {
843-
/* ignore */
844-
}
845-
// Iterate via WeakMap is not directly possible; instead we store wrapped functions that self-record timeout IDs.
846-
// So this helper is best-effort: callers should hold element references if precise flushing is required.
847-
}
848-
849-
// Explicit cancel for a specific element (used when switching modes for active element)
850-
export function cancelDebouncedForElement(element) {
851-
if (!element || typeof element !== 'object') return;
852-
try {
853-
const entry = debouncedCapitalizationMap.get(element);
854-
if (entry && entry.timeoutId) {
855-
clearTimeout(entry.timeoutId);
856-
entry.timeoutId = null;
857-
}
858-
} catch (e) {
859-
// Silently handle WeakMap errors during extension reload
860-
console.debug(
861-
'WeakMap access error (extension may be reloading):',
862-
e.message
863-
);
864-
}
865-
}
866-
867-
export function getDebouncedCapitaliseText(
868-
element,
869-
delay = DEFAULT_DEBOUNCE_DELAY,
870-
capitaliserFn = capitaliseTextProxy
871-
) {
872-
// Defensive check: element must be a valid object for WeakMap
873-
if (!element || typeof element !== 'object') return () => {};
874-
875-
const existing = debouncedCapitalizationMap.get(element);
876-
if (existing && typeof existing.fn === 'function') return existing.fn;
877-
878-
let timeoutId = null;
879-
const wrapped = function (targetElement) {
880-
// Defensive check for extension context invalidation
881-
if (!targetElement || typeof targetElement !== 'object') return;
882-
if (timeoutId) clearTimeout(timeoutId);
883-
// Immediate execute if delay == 0
884-
if (!Number.isFinite(delay) || delay < 0) delay = DEFAULT_DEBOUNCE_DELAY;
885-
if (delay === 0) {
886-
capitaliserFn(
887-
targetElement,
888-
shouldCapitalise,
889-
shouldCapitaliseForI,
890-
getText,
891-
setText
892-
);
893-
return;
894-
}
895-
timeoutId = setTimeout(() => {
896-
timeoutId = null;
897-
try {
898-
capitaliserFn(
899-
targetElement,
900-
shouldCapitalise,
901-
shouldCapitaliseForI,
902-
getText,
903-
setText
904-
);
905-
} catch {
906-
/* ignore */
907-
}
908-
}, delay);
909-
try {
910-
debouncedCapitalizationMap.set(element, { fn: wrapped, timeoutId });
911-
} catch (e) {
912-
// Silently handle WeakMap errors during extension reload
913-
console.debug(
914-
'WeakMap set error (extension may be reloading):',
915-
e.message
916-
);
917-
}
918-
};
919-
920-
try {
921-
debouncedCapitalizationMap.set(element, { fn: wrapped, timeoutId });
922-
} catch (e) {
923-
// Silently handle WeakMap errors during extension reload
924-
console.debug('WeakMap set error (extension may be reloading):', e.message);
925-
}
926-
return wrapped;
927-
}
814+
export const __resetDebouncedMapForTests = _resetDebouncedMapForTests;
815+
export const clearDebouncedCapitalisationCache =
816+
_clearDebouncedCapitalisationCache;
817+
export const flushAndClearDebouncedCapitalisations =
818+
_flushAndClearDebouncedCapitalisations;
819+
export const cancelDebouncedForElement = _cancelDebouncedForElement;
820+
821+
// Compatibility wrapper: adapt existing capitaliser signature to the debounce core's expectation
822+
// Create the compat wrapper via factory to avoid circular imports
823+
export const getDebouncedCapitaliseText = createGetDebouncedCapitaliseText({
824+
getTextFn: getText,
825+
setTextFn: setText,
826+
shouldCapitaliseFn: shouldCapitalise,
827+
shouldCapitaliseForIFn: shouldCapitaliseForI,
828+
capitaliseTextProxyFn: capitaliseTextProxy,
829+
});
928830

929831
// Retroactively apply enabled rules across the entire text of a single element (used when toggling features on)
930832
export function fullReprocessElement(element) {

src/word-mode.js

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,20 +56,20 @@ function isGmailLocal() {
5656

5757
export function getCorrectedWord(caseInsensitive, matchedWord, keyValuePairs) {
5858
if (caseInsensitive === true) {
59-
return keyValuePairs[matchedWord.toLowerCase()]
59+
return keyValuePairs[matchedWord.toLowerCase()];
6060
}
6161

6262
// Case-sensitive lookup first
63-
const direct = keyValuePairs[matchedWord]
64-
if (direct != null) return direct
63+
const direct = keyValuePairs[matchedWord];
64+
if (direct != null) return direct;
6565

6666
// Fallback: try capitalised key (e.g. dict has 'Two' but matchedWord is 'two')
6767
if (matchedWord && matchedWord.length > 0) {
68-
const cap = matchedWord[0].toUpperCase() + matchedWord.slice(1)
69-
return keyValuePairs[cap]
68+
const cap = matchedWord[0].toUpperCase() + matchedWord.slice(1);
69+
return keyValuePairs[cap];
7070
}
7171

72-
return undefined
72+
return undefined;
7373
}
7474

7575
// Core matching function: accepts wordsToExclude so callers can pass shared state
@@ -182,3 +182,34 @@ export function arrayToMapCore(obj) {
182182
return {};
183183
}
184184

185+
// Compatibility wrapper for per-element debounced capitalisation.
186+
// Adapts the debounce core to the higher-level capitaliser used by the rest of the app.
187+
// Factory to create a compatibility wrapper for debounced capitalisation.
188+
// Accepts the portal functions from utils to avoid circular imports.
189+
import { DEFAULT_DEBOUNCE_DELAY } from './debounce';
190+
import { getDebouncedCapitaliseText as _getDebouncedCapitaliseTextCore } from './debounce';
191+
192+
export function createGetDebouncedCapitaliseText({
193+
getTextFn,
194+
setTextFn,
195+
shouldCapitaliseFn,
196+
shouldCapitaliseForIFn,
197+
capitaliseTextProxyFn,
198+
}) {
199+
return function getDebouncedCapitaliseText(
200+
element,
201+
delay = DEFAULT_DEBOUNCE_DELAY,
202+
capitaliserFn = capitaliseTextProxyFn
203+
) {
204+
const wrappedCapitaliser = (el) => {
205+
capitaliserFn(
206+
el,
207+
shouldCapitaliseFn,
208+
shouldCapitaliseForIFn,
209+
getTextFn,
210+
setTextFn
211+
);
212+
};
213+
return _getDebouncedCapitaliseTextCore(element, delay, wrappedCapitaliser);
214+
};
215+
}

0 commit comments

Comments
 (0)