Skip to content

Commit 3ba7a33

Browse files
committed
AG-38132 Improve 'prevent-setTimeout' and 'prevent-setInterval' — support ranges in delay. #467
Squashed commit of the following: commit dc05ef2 Merge: ee9d345 48ebc04 Author: Adam Wróblewski <adam@adguard.com> Date: Tue May 19 10:51:26 2026 +0200 Merge branch 'master' into feature/AG-38132 commit ee9d345 Author: Adam Wróblewski <adam@adguard.com> Date: Wed May 13 16:07:30 2026 +0200 Add information about negative values in delay and add examples for range commit 6e2a332 Author: Adam Wróblewski <adam@adguard.com> Date: Wed May 13 15:04:02 2026 +0200 Add shared test helper for `prevent-setTimeout` and `prevent-setInterval` commit 3e88d4a Author: Adam Wróblewski <adam@adguard.com> Date: Wed May 13 13:37:28 2026 +0200 Add test for negative delay commit 07ac29b Author: Adam Wróblewski <adam@adguard.com> Date: Wed May 13 13:37:02 2026 +0200 Simplify logic in `parseDelayArg`, use `parseFloat` commit 0e4566c Author: Adam Wróblewski <adam@adguard.com> Date: Wed May 13 12:19:37 2026 +0200 Refactor delay matching logic by extracting `isPreventDelayMatched` function commit a68efb1 Author: Adam Wróblewski <adam@adguard.com> Date: Tue May 12 17:15:30 2026 +0200 Enhance delay range validation in `isPreventionNeeded` and `parseDelayArg` functions commit 3b21097 Author: Adam Wróblewski <adam@adguard.com> Date: Tue May 12 14:30:07 2026 +0200 Added support for matching delay by range in `prevent-setTimeout` and `prevent-setInterval`
1 parent 48ebc04 commit 3ba7a33

10 files changed

Lines changed: 1634 additions & 981 deletions

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ The format is based on [Keep a Changelog], and this project adheres to [Semantic
1212

1313
## [Unreleased]
1414

15+
### Added
16+
17+
- Delay range matching for `prevent-setTimeout` and `prevent-setInterval` scriptlets.
18+
Supported formats: `min-max`, `min-`, `-max`, with `!` prefix for inversion [#467].
19+
1520
### Changed
1621

1722
- `prevent-fetch` now supports a structured `responseConfig` argument for overriding synthetic response fields such as
@@ -26,6 +31,7 @@ The format is based on [Keep a Changelog], and this project adheres to [Semantic
2631

2732
[Unreleased]: https://github.com/AdguardTeam/Scriptlets/compare/v2.4.2...HEAD
2833
[#529]: https://github.com/AdguardTeam/Scriptlets/issues/529
34+
[#467]: https://github.com/AdguardTeam/Scriptlets/issues/467
2935

3036
## [2.4.2] - 2026-04-24
3137

src/helpers/prevent-utils.ts

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,43 @@ type PreventData = {
4040
matchDelay: string;
4141
};
4242

43+
/**
44+
* Checks whether the actual delay matches the configured delay condition.
45+
*
46+
* @param isDelayRange whether the delay arg is a range
47+
* @param delayMinMatch minimum delay bound for range match, or null
48+
* @param delayMaxMatch maximum delay bound for range match, or null
49+
* @param delayMatch exact delay to match, or null
50+
* @param isInvertedDelayMatch whether to invert the delay match result
51+
* @param actualDelay parsed delay value from the intercepted call
52+
* @returns whether the delay matches the condition
53+
*/
54+
export const isPreventDelayMatched = (
55+
isDelayRange: boolean,
56+
delayMinMatch: number | null,
57+
delayMaxMatch: number | null,
58+
delayMatch: number | null,
59+
isInvertedDelayMatch: boolean,
60+
actualDelay: unknown,
61+
): boolean => {
62+
if (isDelayRange) {
63+
// Invalid range (e.g. 'abc-100') — both bounds are null, never match
64+
if (delayMinMatch === null && delayMaxMatch === null) {
65+
return false;
66+
}
67+
if (typeof actualDelay !== 'number') {
68+
return false;
69+
}
70+
const aboveMin = delayMinMatch === null || actualDelay >= delayMinMatch;
71+
const belowMax = delayMaxMatch === null || actualDelay <= delayMaxMatch;
72+
return (aboveMin && belowMax) !== isInvertedDelayMatch;
73+
}
74+
if (delayMatch === null) {
75+
return true;
76+
}
77+
return (actualDelay === delayMatch) !== isInvertedDelayMatch;
78+
};
79+
4380
/**
4481
* Checks whether 'callback' and 'delay' are matching
4582
* by given parameters 'matchCallback' and 'matchDelay'.
@@ -64,13 +101,21 @@ export const isPreventionNeeded = ({
64101
if (!isValidCallback(callback)) {
65102
return false;
66103
}
67-
if (!isValidMatchStr(matchCallback)
68-
|| (matchDelay && !isValidMatchNumber(matchDelay))) {
104+
if (
105+
!isValidMatchStr(matchCallback)
106+
|| (matchDelay && !isValidMatchNumber(matchDelay))
107+
) {
69108
return false;
70109
}
71110

72111
const { isInvertedMatch, matchRegexp } = parseMatchArg(matchCallback);
73-
const { isInvertedDelayMatch, delayMatch } = parseDelayArg(matchDelay);
112+
const {
113+
isInvertedDelayMatch,
114+
delayMatch,
115+
delayMinMatch,
116+
delayMaxMatch,
117+
isDelayRange,
118+
} = parseDelayArg(matchDelay);
74119

75120
// Parse delay for decimal, string and non-number values
76121
// https://github.com/AdguardTeam/Scriptlets/issues/247
@@ -79,13 +124,27 @@ export const isPreventionNeeded = ({
79124
let shouldPrevent = false;
80125
// https://github.com/AdguardTeam/Scriptlets/issues/105
81126
const callbackStr = String(callback);
82-
if (delayMatch === null) {
127+
if (!isDelayRange && delayMatch === null) {
83128
shouldPrevent = matchRegexp.test(callbackStr) !== isInvertedMatch;
84129
} else if (!matchCallback) {
85-
shouldPrevent = (parsedDelay === delayMatch) !== isInvertedDelayMatch;
130+
shouldPrevent = isPreventDelayMatched(
131+
isDelayRange,
132+
delayMinMatch,
133+
delayMaxMatch,
134+
delayMatch,
135+
isInvertedDelayMatch,
136+
parsedDelay,
137+
);
86138
} else {
87139
shouldPrevent = matchRegexp.test(callbackStr) !== isInvertedMatch
88-
&& (parsedDelay === delayMatch) !== isInvertedDelayMatch;
140+
&& isPreventDelayMatched(
141+
isDelayRange,
142+
delayMinMatch,
143+
delayMaxMatch,
144+
delayMatch,
145+
isInvertedDelayMatch,
146+
parsedDelay,
147+
);
89148
}
90149
return shouldPrevent;
91150
};

src/helpers/string-utils.ts

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ type MatchData = {
2222
type DelayData = {
2323
isInvertedDelayMatch: boolean;
2424
delayMatch: number | null;
25+
delayMinMatch: number | null;
26+
delayMaxMatch: number | null;
27+
isDelayRange: boolean;
2528
};
2629

2730
/**
@@ -264,6 +267,21 @@ export const isValidMatchNumber = (match: RawStrPattern): boolean => {
264267
if (match?.startsWith(INVERT_MARKER)) {
265268
str = match.slice(1);
266269
}
270+
// Check for range pattern: 'min-max', 'min-', '-max'
271+
const RANGE_SEPARATOR = '-';
272+
const separatorIndex = str.indexOf(RANGE_SEPARATOR);
273+
if (separatorIndex !== -1) {
274+
const minStr = str.slice(0, separatorIndex);
275+
const maxStr = str.slice(separatorIndex + 1);
276+
const isMinValid = minStr === '' || (!nativeIsNaN(parseFloat(minStr)) && nativeIsFinite(parseFloat(minStr)));
277+
const isMaxValid = maxStr === '' || (!nativeIsNaN(parseFloat(maxStr)) && nativeIsFinite(parseFloat(maxStr)));
278+
// At least one bound must be specified and both parts must be valid
279+
if (isMinValid && isMaxValid && (minStr !== '' || maxStr !== '')) {
280+
return true;
281+
}
282+
// If separator is present but range is invalid, reject it
283+
return false;
284+
}
267285
const num = parseFloat(str);
268286
return !nativeIsNaN(num) && nativeIsFinite(num);
269287
};
@@ -294,11 +312,49 @@ export const parseMatchArg = (match: string): MatchData => {
294312
*/
295313
export const parseDelayArg = (delay: string): DelayData => {
296314
const INVERT_MARKER = '!';
315+
const RANGE_SEPARATOR = '-';
297316
const isInvertedDelayMatch = delay?.startsWith(INVERT_MARKER);
298317
const delayValue = isInvertedDelayMatch ? delay.slice(1) : delay;
318+
319+
// Check for range pattern: 'min-max', 'min-', '-max'
320+
const separatorIndex = delayValue ? delayValue.indexOf(RANGE_SEPARATOR) : -1;
321+
322+
if (separatorIndex !== -1) {
323+
const minStr = delayValue.slice(0, separatorIndex);
324+
const maxStr = delayValue.slice(separatorIndex + 1);
325+
const delayMinMatch = minStr === '' ? null : parseFloat(minStr);
326+
const delayMaxMatch = maxStr === '' ? null : parseFloat(maxStr);
327+
const minValid = minStr === '' || (delayMinMatch !== null && !nativeIsNaN(delayMinMatch));
328+
const maxValid = maxStr === '' || (delayMaxMatch !== null && !nativeIsNaN(delayMaxMatch));
329+
if (minValid && maxValid && (delayMinMatch !== null || delayMaxMatch !== null)) {
330+
return {
331+
isInvertedDelayMatch,
332+
delayMatch: null,
333+
delayMinMatch,
334+
delayMaxMatch,
335+
isDelayRange: true,
336+
};
337+
}
338+
// Separator present but range is invalid — still mark as range
339+
// so isPreventionNeeded can reject it
340+
return {
341+
isInvertedDelayMatch,
342+
delayMatch: null,
343+
delayMinMatch: null,
344+
delayMaxMatch: null,
345+
isDelayRange: true,
346+
};
347+
}
348+
299349
const parsedDelay = parseInt(delayValue, 10);
300350
const delayMatch = nativeIsNaN(parsedDelay) ? null : parsedDelay;
301-
return { isInvertedDelayMatch, delayMatch };
351+
return {
352+
isInvertedDelayMatch,
353+
delayMatch,
354+
delayMinMatch: null,
355+
delayMaxMatch: null,
356+
isDelayRange: false,
357+
};
302358
};
303359

304360
/**

src/scriptlets/prevent-setInterval.js

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
hit,
33
noopFunc,
44
isPreventionNeeded,
5+
isPreventDelayMatched,
56
logMessage,
67
toRegExp,
78
nativeIsNaN,
@@ -44,10 +45,13 @@ import {
4445
* If starts with `!`, scriptlet will not match the stringified callback but all other will be defused.
4546
* If do not start with `!`, the stringified callback will be matched.
4647
* If not set, prevents all `setInterval` calls due to specified `matchDelay`.
47-
* - `matchDelay` — optional, must be an integer.
48+
* - `matchDelay` — optional, must be an integer or a delay range.
4849
* If starts with `!`, scriptlet will not match the delay but all other will be defused.
4950
* If do not start with `!`, the delay passed to the `setInterval` call will be matched.
5051
* Decimal delay values will be rounded down, e.g `10.95` will be matched by `matchDelay` with value `10`.
52+
* Delay ranges are supported in the format `min-max` (matches if `min <= delay <= max`),
53+
* `min-` (matches if `delay >= min`), or `-max` (matches if `delay <= max`).
54+
* Negative delay values behave the same as `0`, so use `-0` to match them.
5155
*
5256
* > If `prevent-setInterval` log looks like `setInterval(undefined, 1000)`,
5357
* > it means that no callback was passed to setInterval() and that's not scriptlet issue
@@ -149,6 +153,54 @@ import {
149153
* }, 300 + Math.random());
150154
* ```
151155
*
156+
* 1. Prevents `setInterval` calls if the delay is in the `20-50` range
157+
*
158+
* ```adblock
159+
* example.org#%#//scriptlet('prevent-setInterval', '', '20-50')
160+
* ```
161+
*
162+
* For instance, only the second of the following calls will be prevented:
163+
*
164+
* ```javascript
165+
* setInterval(function () {
166+
* window.test = "10 -- executed";
167+
* }, 10);
168+
* setInterval(function () {
169+
* window.test = "30 -- prevented";
170+
* }, 30);
171+
* ```
172+
*
173+
* 1. Prevents `setInterval` calls if the delay is at least `30`
174+
*
175+
* ```adblock
176+
* example.org#%#//scriptlet('prevent-setInterval', '', '30-')
177+
* ```
178+
*
179+
* For instance, only the second of the following calls will be prevented:
180+
*
181+
* ```javascript
182+
* setInterval(function () {
183+
* window.test = "10 -- executed";
184+
* }, 10);
185+
* setInterval(function () {
186+
* window.test = "60 -- prevented";
187+
* }, 60);
188+
* ```
189+
*
190+
* 1. Prevents `setInterval` calls if the delay is negative
191+
*
192+
* ```adblock
193+
* example.org#%#//scriptlet('prevent-setInterval', '', '-0')
194+
* ```
195+
*
196+
* For instance, the following call will be prevented:
197+
*
198+
* ```javascript
199+
* setInterval(function () {
200+
* window.test = "negative -- prevented";
201+
* }, -10);
202+
* ```
203+
*
152204
* @added v1.0.4.
153205
*/
154206
/* eslint-enable max-len */
@@ -216,6 +268,7 @@ preventSetInterval.injections = [
216268
nativeIsNaN,
217269
parseMatchArg,
218270
parseDelayArg,
271+
isPreventDelayMatched,
219272
isValidCallback,
220273
isValidMatchStr,
221274
isValidStrPattern,

src/scriptlets/prevent-setTimeout.js

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
hit,
33
noopFunc,
44
isPreventionNeeded,
5+
isPreventDelayMatched,
56
logMessage,
67
parseMatchArg,
78
parseDelayArg,
@@ -44,10 +45,13 @@ import {
4445
* If starts with `!`, scriptlet will not match the stringified callback but all other will be defused.
4546
* If do not start with `!`, the stringified callback will be matched.
4647
* If not set, prevents all `setTimeout` calls due to specified `matchDelay`.
47-
* - `matchDelay` — optional, must be an integer.
48+
* - `matchDelay` — optional, must be an integer or a delay range.
4849
* If starts with `!`, scriptlet will not match the delay but all other will be defused.
4950
* If do not start with `!`, the delay passed to the `setTimeout` call will be matched.
5051
* Decimal delay values will be rounded down, e.g `10.95` will be matched by `matchDelay` with value `10`.
52+
* Delay ranges are supported in the format `min-max` (matches if `min <= delay <= max`),
53+
* `min-` (matches if `delay >= min`), or `-max` (matches if `delay <= max`).
54+
* Negative delay values behave the same as `0`, so use `-0` to match them.
5155
*
5256
* > If `prevent-setTimeout` log looks like `setTimeout(undefined, 1000)`,
5357
* > it means that no callback was passed to setTimeout() and that's not scriptlet issue
@@ -149,6 +153,54 @@ import {
149153
* }, 300 + Math.random());
150154
* ```
151155
*
156+
* 1. Prevents `setTimeout` calls if the delay is in the `20-50` range
157+
*
158+
* ```adblock
159+
* example.org#%#//scriptlet('prevent-setTimeout', '', '20-50')
160+
* ```
161+
*
162+
* For instance, only the second of the following calls will be prevented:
163+
*
164+
* ```javascript
165+
* setTimeout(function () {
166+
* window.test = "10 -- executed";
167+
* }, 10);
168+
* setTimeout(function () {
169+
* window.test = "30 -- prevented";
170+
* }, 30);
171+
* ```
172+
*
173+
* 1. Prevents `setTimeout` calls if the delay is at least `30`
174+
*
175+
* ```adblock
176+
* example.org#%#//scriptlet('prevent-setTimeout', '', '30-')
177+
* ```
178+
*
179+
* For instance, only the second of the following calls will be prevented:
180+
*
181+
* ```javascript
182+
* setTimeout(function () {
183+
* window.test = "10 -- executed";
184+
* }, 10);
185+
* setTimeout(function () {
186+
* window.test = "60 -- prevented";
187+
* }, 60);
188+
* ```
189+
*
190+
* 1. Prevents `setTimeout` calls if the delay is negative
191+
*
192+
* ```adblock
193+
* example.org#%#//scriptlet('prevent-setTimeout', '', '-0')
194+
* ```
195+
*
196+
* For instance, the following call will be prevented:
197+
*
198+
* ```javascript
199+
* setTimeout(function () {
200+
* window.test = "negative -- prevented";
201+
* }, -10);
202+
* ```
203+
*
152204
* @added v1.0.4.
153205
*/
154206
/* eslint-enable max-len */
@@ -217,6 +269,7 @@ preventSetTimeout.injections = [
217269
// following helpers should be injected as helpers above use them
218270
parseMatchArg,
219271
parseDelayArg,
272+
isPreventDelayMatched,
220273
toRegExp,
221274
nativeIsNaN,
222275
isValidCallback,

0 commit comments

Comments
 (0)