I'm using BreezeJS to handle server-side filtering of an ng-grid. To do this, I'm just $watch'ing for changes to the ng-grid filter, and refreshing my data with BreezeJS. If I type fast enough though, the AJAX queries can end up coming back out of order.

I was thinking about associating a GUID with each query (via closure), and keeping track of the GUID of the last query request that was made. Then ignoring any queries that returned that didn't match that GUID (I don't need outdated query results - just the most recent).

But is there was a better, or more idiomatic way of handling this, particularly in Angular/Breeze?

有帮助吗?

解决方案

@Adam is on to an important idea for your primary problem ... taming the flurry of queries that you're firing off as users type in the search box.

Debounce user input

You should "debounce" (AKA, "throttle") the search entry before you start canceling requests. That means waiting for the user to stop typing before you issue a query to the server.

Search the web for "Angular" and "Debounce" and you'll discover a number of techniques. One I used was to bind the search box to ngChanged=vm.searchChanged(). The ViewModel's searchChanged method would then start a $timeout. If the user entered anything within 1/2 a second, I'd cancel that timeout and start another. After 500ms of silence I'd start a query. I'd also start immediately on blur or if they hit the enter key.

When Angular v.1.3 is released (very soon now), "debounce" will be part of Angular binding. I'm looking forward to trashing my homebrew debounce code.

Cancel

Imagine the user stops typing for 500ms, the query starts, ... and the impatient user wants to cancel the request. She can't do that in Breeze v.1.4.11. She will be able to in v.1.4.12.

I just extended the Breeze AJAX adapters for jQuery and Angular to facilitate both cancel and timeout in response to another question of yours.

Response order

There are other situations in which you launch multiple requests. The responses won't necessarily arrive in the order of your requests. That's the nature of async.

You absolutely can keep the order straight yourself. Remember that you construct the callbacks. You can maintain an application-wide request counter that you increment for each Query and Save ... and then reference in your callbacks.

I wrote an illustrative example in DocCode:queryTests.js that shows one way to do it:

/*********************************************************
* Dealing with response order
* It's difficult to make the server flip the response order
* (run it enough times and the response order will flip)
* but the logic of this test manifestly deals with it
* because of the way it assigns results.
*********************************************************/
asyncTest("can sequence results that arrive out of order", 3, function() {
    var nextRequestId = 0;
    var em = newEm();
    var promises = [];
    var results = [];
    var arrived = [];

    promises.push(breeze.EntityQuery.from('Customers')
        .where('CompanyName', 'startsWith', 'a')
        .using(em).execute()
        .then(makeSuccessFn()).catch(handleFail));

    promises.push(breeze.EntityQuery.from('Customers')
        .where('CompanyName', 'startsWith', 's')
        .using(em).execute()
        .then(makeSuccessFn()).catch(handleFail));

    function makeSuccessFn() {
        var requestId = nextRequestId++;
        return function success(data) {
            // Don't know which response arrived first?
            // Sure you do. Just refer to the requestId which is a capture
            arrived.push(requestId);
            results[requestId] = data.results;
            assertWhenDone();
        }
    }

    function assertWhenDone() {
        if (results[0] && results[1]) {
            start(); // we're done
            // Here we report the actual response order
            ok(true, "Request #{0} arrived before #{1}".format(arrived[0], arrived[1]));
            // no matter what the response order
            // the 'a' companies go in results slot #0
            // the 's' companies go in results slot #1
            var aCompany = results[0][1].CompanyName();
            var sCompany = results[1][1].CompanyName();
            equal(aCompany[0].toLowerCase(), 'a',
                "company from first batch should be an 'a', was " + aCompany);
            equal(sCompany[0].toLowerCase(), 's',
                "company from second batch should be an 's', was " + sCompany);
        }
    }
});

Update 21 Jan 2015

I should have mentioned that the all promises method passes a response array to the then(...) success callback that preserves the request order. If you happen to issue more than one query at the same time in the same place and can await them together (as in the example above), you don't need all of the heavy lifting of the requestId. Just do this ...

var promises = [];

promises.push(breeze.EntityQuery.from('Customers')
        .where('CompanyName', 'startsWith', 'a')
        .using(em).execute();

promises.push(breeze.EntityQuery.from('Customers')
        .where('CompanyName', 'startsWith', 's')
        .using(em).execute();

// Q.all waits until all succeed or one of them fails
// breeze.Q is typically $q in an Angular app
breeze.Q.all(promises).then(allSucceeded).catch(atLeastOneFail);

function allSucceeded(responses) {
   // response[0] is from the first 'a' query regardless of when it finished.
   // response[1] is from the second 's' query regardless of when it finished.      
}

其他提示

Cancel any previous request as the user is typing, that way, only the response to the current request, if it gets completed, will update the results.

Pseudo code only:

var req;

function search(searchTerm) {
     req && req.cancel();
     req = queryServer().then(resultsReturned);
}

function resultsReturned() {
}
许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top