Skip to content
This repository was archived by the owner on Feb 22, 2018. It is now read-only.

Commit b4e0d53

Browse files
deploy-test@travis-ci.orgchirayuk
authored andcommitted
feat(http): support coalescing http requests
To enable this, configure the HttpConfig module with a coalescing duration. Doing so will enable all HTTP responses that arrive within that interval to all be processed in a single digest. Typically, this is most helpful if you have many XHRs that are all going to be satisfied by cached responses causing them to happen in quick succession. e.g. Use something like this to configure a 50ms interval. bind(HttpConfig, toValue: new HttpConfig( coalesceDuration: new Duration(milliseconds: 50)));
1 parent 42ed6a8 commit b4e0d53

File tree

5 files changed

+123
-34
lines changed

5 files changed

+123
-34
lines changed

lib/core/module.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export "package:angular/core_dom/module_internal.dart" show
4343
EventHandler,
4444
Http,
4545
HttpBackend,
46+
HttpConfig,
4647
HttpDefaultHeaders,
4748
HttpDefaults,
4849
HttpInterceptor,

lib/core_dom/http.dart

Lines changed: 87 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ typedef RequestInterceptor(HttpResponseConfig);
3838
typedef RequestErrorInterceptor(dynamic);
3939
typedef Response(HttpResponse);
4040
typedef ResponseError(dynamic);
41+
typedef _CompleteResponse(HttpResponse);
42+
typedef _RunCoaleced(fn());
43+
44+
_runNow(fn()) => fn();
45+
_identity(x) => x;
4146

4247
/**
4348
* HttpInterceptors are used to modify the Http request. They can be added to
@@ -369,23 +374,29 @@ class HttpDefaults {
369374
*/
370375
@Injectable()
371376
class Http {
372-
var _pendingRequests = new HashMap<String, async.Future<HttpResponse>>();
373-
BrowserCookies _cookies;
374-
LocationWrapper _location;
375-
UrlRewriter _rewriter;
376-
HttpBackend _backend;
377-
HttpInterceptors _interceptors;
377+
final _pendingRequests = new HashMap<String, async.Future<HttpResponse>>();
378+
final BrowserCookies _cookies;
379+
final LocationWrapper _location;
380+
final UrlRewriter _rewriter;
381+
final HttpBackend _backend;
382+
final HttpInterceptors _interceptors;
383+
final RootScope _rootScope;
384+
final HttpConfig _httpConfig;
385+
final VmTurnZone _zone;
386+
387+
final _responseQueue = <Function>[];
388+
async.Timer _responseQueueTimer;
378389

379390
/**
380391
* The defaults for [Http]
381392
*/
382-
HttpDefaults defaults;
393+
final HttpDefaults defaults;
383394

384395
/**
385396
* Constructor, useful for DI.
386397
*/
387-
Http(this._cookies, this._location, this._rewriter, this._backend,
388-
this.defaults, this._interceptors);
398+
Http(this._cookies, this._location, this._rewriter, this._backend, this.defaults,
399+
this._interceptors, this._rootScope, this._httpConfig, this._zone);
389400

390401
/**
391402
* Parse a [requestUrl] and determine whether this is a same-origin request as
@@ -481,29 +492,25 @@ class Http {
481492
return new async.Future.value(new HttpResponse.copy(cachedResponse));
482493
}
483494

484-
var result = _backend.request(url,
485-
method: method,
486-
requestHeaders: config.headers,
487-
sendData: config.data,
488-
withCredentials: withCredentials).then((dom.HttpRequest value) {
489-
// TODO: Uncomment after apps migrate off of this class.
490-
// assert(value.status >= 200 && value.status < 300);
491-
492-
var response = new HttpResponse(value.status, value.responseText,
493-
parseHeaders(value), config);
494-
495-
if (cache != null) cache.put(url, response);
496-
_pendingRequests.remove(url);
497-
return response;
498-
}, onError: (error) {
499-
if (error is! dom.ProgressEvent) throw error;
500-
dom.ProgressEvent event = error;
501-
_pendingRequests.remove(url);
502-
dom.HttpRequest request = event.currentTarget;
503-
return new async.Future.error(
504-
new HttpResponse(request.status, request.response, parseHeaders(request), config));
505-
});
506-
return _pendingRequests[url] = result;
495+
requestFromBackend(runCoalesced, onComplete, onError) => _backend.request(
496+
url,
497+
method: method,
498+
requestHeaders: config.headers,
499+
sendData: config.data,
500+
withCredentials: withCredentials
501+
).then((dom.HttpRequest req) => _onResponse(req, runCoalesced, onComplete, config, cache, url),
502+
onError: (e) => _onError(e, runCoalesced, onError, config, url));
503+
504+
async.Future responseFuture;
505+
if (_httpConfig.coalesceDuration != null) {
506+
async.Completer completer = new async.Completer();
507+
responseFuture = completer.future;
508+
_zone.runOutsideAngular(() => requestFromBackend(
509+
_coalesce, completer.complete, completer.completeError));
510+
} else {
511+
responseFuture = requestFromBackend(_runNow, _identity, _identity);
512+
}
513+
return _pendingRequests[url] = responseFuture;
507514
};
508515

509516
var chain = [[serverRequest, null]];
@@ -650,11 +657,50 @@ class Http {
650657
xsrfCookieName: xsrfCookieName, interceptors: interceptors, cache: cache,
651658
timeout: timeout);
652659

660+
_onResponse(dom.HttpRequest request, _RunCoaleced runCoalesced, _CompleteResponse onComplete,
661+
HttpResponseConfig config, cache, String url) {
662+
// TODO: Uncomment after apps migrate off of this class.
663+
// assert(request.status >= 200 && request.status < 300);
664+
665+
var response = new HttpResponse(
666+
request.status, request.responseText, parseHeaders(request), config);
667+
668+
if (cache != null) cache.put(url, response);
669+
_pendingRequests.remove(url);
670+
return runCoalesced(() => onComplete(response));
671+
}
672+
673+
_onError(error, _RunCoaleced runCoalesced, _CompleteResponse onError,
674+
HttpResponseConfig config, String url) {
675+
if (error is! dom.ProgressEvent) throw error;
676+
dom.ProgressEvent event = error;
677+
_pendingRequests.remove(url);
678+
dom.HttpRequest request = event.currentTarget;
679+
var response = new HttpResponse(
680+
request.status, request.response, parseHeaders(request), config);
681+
return runCoalesced(() => onError(new async.Future.error(response)));
682+
}
683+
684+
_coalesce(fn()) {
685+
_responseQueue.add(fn);
686+
if (_responseQueueTimer == null) {
687+
_responseQueueTimer = new async.Timer(_httpConfig.coalesceDuration, _flushResponseQueue);
688+
}
689+
}
690+
691+
_flushResponseQueue() => _zone.run(_flushResponseQueueSync);
692+
693+
_flushResponseQueueSync() {
694+
_responseQueueTimer = null;
695+
_responseQueue.forEach(_runNow);
696+
_responseQueue.clear();
697+
}
698+
653699
/**
654700
* Parse raw headers into key-value object
655701
*/
656-
static Map<String, String> parseHeaders(dom.HttpRequest value) {
657-
var headers = value.getAllResponseHeaders();
702+
static Map<String, String> parseHeaders(dom.HttpRequest request) {
703+
var headers = request.getAllResponseHeaders();
658704

659705
var parsed = new HashMap();
660706

@@ -704,3 +750,10 @@ class Http {
704750
.replaceAll('%2C', ',')
705751
.replaceAll('%20', pctEncodeSpaces ? '%20' : '+');
706752
}
753+
754+
@Injectable()
755+
class HttpConfig {
756+
final Duration coalesceDuration;
757+
758+
HttpConfig({this.coalesceDuration});
759+
}

lib/core_dom/module_internal.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ class CoreDomModule extends Module {
8686
bind(HttpDefaultHeaders);
8787
bind(HttpDefaults);
8888
bind(HttpInterceptors);
89+
bind(HttpConfig, toValue: new HttpConfig());
8990
bind(Animate);
9091
bind(ViewCache);
9192
bind(BrowserCookies);

test/angular_spec.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ main() {
127127
"angular.core.dom_internal.EventHandler",
128128
"angular.core.dom_internal.Http",
129129
"angular.core.dom_internal.HttpBackend",
130+
"angular.core.dom_internal.HttpConfig",
130131
"angular.core.dom_internal.HttpDefaultHeaders",
131132
"angular.core.dom_internal.HttpDefaults",
132133
"angular.core.dom_internal.HttpInterceptor",

test/core_dom/http_spec.dart

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1407,6 +1407,39 @@ void main() {
14071407
});
14081408
});
14091409
});
1410+
1411+
describe('coalesce', () {
1412+
beforeEachModule((Module module) {
1413+
var coalesceDuration = new Duration(milliseconds: 100);
1414+
module.bind(HttpConfig, toValue: new HttpConfig(coalesceDuration: coalesceDuration));
1415+
});
1416+
1417+
it('should coalesce requests', async((Http http) {
1418+
backend.expect('GET', '/foo').respond(200, 'foo');
1419+
backend.expect('GET', '/bar').respond(200, 'bar');
1420+
1421+
var fooResp, barResp;
1422+
http.get('/foo').then((HttpResponse resp) => fooResp = resp.data);
1423+
http.get('/bar').then((HttpResponse resp) => barResp = resp.data);
1424+
1425+
microLeap();
1426+
backend.flush();
1427+
microLeap();
1428+
expect(fooResp).toBeNull();
1429+
expect(barResp).toBeNull();
1430+
1431+
clockTick(milliseconds: 99);
1432+
microLeap();
1433+
expect(fooResp).toBeNull();
1434+
expect(barResp).toBeNull();
1435+
1436+
clockTick(milliseconds: 1);
1437+
microLeap();
1438+
expect(fooResp).toEqual('foo');
1439+
expect(barResp).toEqual('bar');
1440+
}));
1441+
1442+
});
14101443
});
14111444
}
14121445

0 commit comments

Comments
 (0)