Fusetools bouncer

      No Comments on Fusetools bouncer

It is easy to over flood the API with useless calls. Not only the calls are the problem, but the time and energy spent to execute the action before and after the API call.

Use case

User uses mobile app, enters search criterion in the input field. After the third character entered, results from API are pulled and rendered on screen. Simple isn’t it? But lets inspect the use case and try to find the problem.

The code

Using Fusetools Observable of string. Action is fired every time field changes.

The ViewModel

1
2
3
4
5
6
var searchInputObs = Observable("");

--react on input field changes
searchInputObs.onValueChanged(module, function(criterion){
     someactionstodo(criterion);
});

The View, assign Observable to the field

1
2
3
4
5
<TextInput ux:Name="text" Value="{searchInputObs}" PlaceholderText="Find places" PlaceholderColor="#ccc" Height="40" Padding="5" >
    <Rectangle Layer="Background">
        <Stroke Width="1" Brush="#BBB" />
    </Rectangle>
</TextInput>
Results

Not bad for POC (Proof of concept), but bad for production and here is why.
Inspecting “api/search” endpoint. Immense amount of requests on endpoint, which is something to be expected.

Logs:

1. acc;
2. acco;
3. accom
4. accomm;
5. accommo;
6. accommod;
7. accommoda;
8. accommodat;
9. accommodati;
10. accommodatio;
11. accommodation;
12. accommodation i;
13. accommodation in;
14. accommodation in R;
15. accommodation in Ri;
16. accommodation in Rig;
17. accommodation in Riga.

Seventeen times, the API endpoint was hit to find “accommodation in Riga”. In good scenario, query will hit the cache, in bad the database, other micro services, services or whatever engine, which provides the search. Every time response arrives, it is rendered on mobile screen. Expensive thing to do, isn’t it?

Solution

Add a bouncer before the door, and don’t make the club overcrowded, so it is actually pleasant to be in the club. And if someone is misbehaving, bounce them off.

If Vin can do it, You can do it or #justdoit.

Found Davids blog, how to debounce in JavaScript. Code is from Underscore.js library.

1
_.debounce(function, wait, [immediate])

Creates and returns a new debounced version of the passed function which will postpone its execution until after wait milliseconds have elapsed since the last time it was invoked. Useful for implementing behavior that should only happen after the input has stopped arriving. For example: rendering a preview of a Markdown comment, recalculating a layout after the window has stopped being resized, and so on.

At the end of the wait interval, the function will be called with the arguments that were passed most recently to the debounced function.

Pass true for the immediate argument to cause debounce to trigger the function on the leading instead of the trailing edge of the wait interval. Useful in circumstances like preventing accidental double-clicks on a “submit” button from firing a second time.

1
2
var lazyLayout = _.debounce(calculateLayout, 300);
$(window).resize(lazyLayout);

This would work for the use case. Get the last state of user input “accommodation in Riga”, without hitting the API seventeen times. But I want something more async.

Async menthod

Original debounce, execute the wrapped method, but I want the result of executed method. I’m thinking about Promises.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function debounceAsync(func, wait) {
    var timeout;
    var fn = function() {
        var context = this, args = arguments;
        return new Promise(function(resolve, reject) {
            var later = function() {
                timeout = null;
                resolve(func.apply(context, args));
            };

            if (timeout) {
                reject("vin - debounce");
                clearTimeout(timeout);
            }

            timeout = setTimeout(later, wait);
        });
    };
    return fn;
};

module.exports = {
    debounceAsync: debounceAsync
}

Get a Promise and act on promise result, good stuff.
Wait time, before execute the API call, is 600ms. If user enters next character within 600ms, previous request will be debounced (rejected) and will not hit the API endpoint.

Model
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
function SearchUtil() {
    var self = this;
    this.createSearchResult = function(searchResults) {
        var result = [];
        searchResults.forEach(function(p){
            var item = {
                id: p.id,
                name: p.name
            }
            result.push(item);
        });
        return result;
    };
   
    this.search = debounceAsync(function(arrayOfCriterion) {
        return APIService.SearchPost("api/search", arrayOfCriterion).then(function(searchResults) {
            return self.createSearchResult(searchResults);
        });
    }, 600);
};

var searchUtil = new SearchUtil();
var searchItemsObs = Observable();
function Search(criterion) {
    return searchUtil.search(criterion).then(function(arrayOfSearchResult){
        if (arrayOfSearchResult) {
            searchResultObs.replaceAll(arrayOfSearchResult);
            return true;
        }
        return false;
    }).catch(function(error) {
        console.log(error);
    });
}

function InitSearchResult() {
    return searchItemsObs;
}

module.exports = {
    Search: Search,
    InitSearchResult: InitSearchResult
}
ViewModel
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var SearchModel = require("search.model");

var searchInputObs = Observable("");
var searchItems = SearchModel.InitSearchResult();

searchInputObs.onValueChanged(module, function(criterion){
    if (criterion.length > 3) {
        isBusy.activate();
        SearchModel.Search(criterion).then(function(){
            isBusy.deactivate();
        });
    } else {
        searchItems.clear();
    }
});

If user is fast typer, logs will indicate that 🙂

[Viewport]: “vin – debounce”
[Viewport]: “vin – debounce”
[Viewport]: “vin – debounce”

View

Spinner is taken from fusetools examples.

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
<Page ux:Class="SearchPage">
    <Router ux:Dependency="router" />

    <JavaScript File="search.view.model.js" />
   
   
    <Panel ux:Class="MyLoadingIndicator" ThemeColor="#1565C0">
        <float4 ux:Property="ThemeColor" />
        <Circle ux:Name="rotatingStroke" Width="50" Height="50" StartAngleDegrees="-45" EndAngleDegrees="45">
            <Stroke Width="2" Color="{ReadProperty ThemeColor}" />
        </Circle>
        <Circle Color="{ReadProperty ThemeColor}" Width="20" Height="20">
            <Timeline ux:Name="myTimeline">
                <Scale Factor=".5" Duration=".25" Easing="CircularOut" EasingBack="CircularIn" />
                <Scale Factor="2" Duration=".25" Delay=".25" Easing="BounceOut" EasingBack="BounceIn" />
            </Timeline>
        </Circle>
        <WhileFalse>
            <Cycle Target="myTimeline.TargetProgress" Low="0" High="1" Frequency=".5" />
            <Spin Target="rotatingStroke" Frequency="1" />
        </WhileFalse>
    </Panel>
   
    <DockPanel>
        <Busy ux:Name="isBusy" IsActive="false" />
        <WhileBusy>
            <Change loadingPanel.Opacity="1" Duration=".4" />
        </WhileBusy>
        <MyLoadingIndicator ux:Name="loadingPanel" Opacity="0" />
   
        <Panel Dock="Top">
            <TextInput ux:Name="text" Value="{searchInputObs}" PlaceholderText="Find places" PlaceholderColor="#ccc" Height="40" Padding="5" >
                <Rectangle Layer="Background">
                    <Stroke Width="1" Brush="#BBB" />
                </Rectangle>
            </TextInput>
        </Panel>
        <ScrollView>
            <StackPanel ItemSpacing="10">
                <Each Items="{searchItemsObs}">
                    <StackPanel Orientation="Horizontal" ItemSpacing="3">
                        <Text>{id}</Text>
                        <Text>{name}</Text>
                    </StackPanel>
                </Each>
            </StackPanel>
        </ScrollView>
    </DockPanel>
<Page>

Voila! This will cool down the things a little bit and your solution will live another day.

Leave a Reply

Your email address will not be published. Required fields are marked *