diff --git a/src/app/app.js b/src/app/app.js
index 8828e6ab..657ea08f 100644
--- a/src/app/app.js
+++ b/src/app/app.js
@@ -205,8 +205,8 @@ var app = angular.module('baw',
}])
- .run(['$rootScope', '$location', '$route', '$http', 'Authenticator', 'AudioEvent', 'conf.paths', 'UserProfile', 'ngAudioEvents', '$url',
- function ($rootScope, $location, $route, $http, Authenticator, AudioEvent, paths, UserProfile, ngAudioEvents, $url) {
+ .run(['$rootScope', '$location', '$route', '$http', 'Authenticator', 'AudioEvent', 'conf.paths', 'UserProfile', 'ngAudioEvents', '$url', "predictiveCache", "conf.constants",
+ function ($rootScope, $location, $route, $http, Authenticator, AudioEvent, paths, UserProfile, ngAudioEvents, $url, predictiveCache, constants) {
// embed configuration for easy site-wide binding
$rootScope.paths = paths;
@@ -343,6 +343,9 @@ var app = angular.module('baw',
$rootScope.downloadAnnotationLink = AudioEvent.csvLink();
+ // set up predictive cache service
+ predictiveCache(constants.predictiveCache.profiles["Media cache ahead"]($location, paths));
+
}])
.controller('AppCtrl',
diff --git a/src/app/listen/listen.js b/src/app/listen/listen.js
index 6150ab74..ca6d02b3 100644
--- a/src/app/listen/listen.js
+++ b/src/app/listen/listen.js
@@ -411,7 +411,7 @@ angular.module('bawApp.listen', ['decipher.tags', 'ui.bootstrap.typeahead'])
return undefined;
}
- return moment($scope.model.media.recordedDate).add($scope.jumpToMinute, 'm').format("YYYY-MMM-DD, HH:mm:ss");
+ return moment($scope.model.media.recordedDatep).add($scope.jumpToMinute, 'm').format("YYYY-MMM-DD, HH:mm:ss");
};
diff --git a/src/baw.configuration.tpl.js b/src/baw.configuration.tpl.js
index a8eca4af..d1dca48e 100644
--- a/src/baw.configuration.tpl.js
+++ b/src/baw.configuration.tpl.js
@@ -116,9 +116,7 @@ angular.module('bawApp.configuration', ['url'])
libraryItem: "/library/{recordingId}/audio_events/{audioEventId}"
},
// general links for use in 's
- links: {
-
- }
+ links: {}
}
};
@@ -229,7 +227,7 @@ angular.module('bawApp.configuration', ['url'])
chrome: 30,
safari: 5.1,
opera: 23,
- ios: 5.1,
+ ios: 5.1,
android: 4.0
},
baseMessage: "Your current internet browser ({name}, version {version}) is {reason}.
Consider updating or try using Google Chrome.",
@@ -246,29 +244,61 @@ angular.module('bawApp.configuration', ['url'])
},
predictiveCache: {
- profiles: [
- {
- name: "Media cache ahead",
- match: "some url",
- request: ["one url", "another url"],
- progression: [
- function(data, previous) {
- return previous + 30.0;
- },
- function(data, previous) {
- var next = previous + 30.0;
- if (next >= data.max) {
- return;
- }
- else {
- return next;
- }
+ profiles: {
+ "Media cache ahead": function bind($location, paths) {
+ // request additional bits of media based the duration of the original request
+ // do not make requests that would exceed the end of the recording
+ function mediaProgressor(previous, data) {
+ var media = data.responseData.data,
+ duration = media.commonParameters.endOffset - media.commonParameters.startOffset,
+ next = previous + duration,
+ max = media.recording.durationSeconds;
+
+ if (next >= max) {
+ return;
+ }
+ else {
+ return next;
}
- ],
- count: 10,
- method: "HEAD"
+ }
+
+ function formatMediaUrl(url, counters) {
+ return paths.api.root + url
+ .replace(/start_offset=[\.\d]+/, "start_offset=" + counters[0])
+ .replace(/end_offset=[\.\d]+/, "end_offset=" + counters[1]);
+ }
+ return {
+ name: "Media cache ahead",
+ match: function (url, response) {
+ // match only if on listen page and request is for a media's json
+ if ($location.path().indexOf("/listen") === 0 &&
+ /\/audio_recordings\/[\.\d]+\/media\.json.*/.test(url)) {
+ var so = response.config.params.start_offset;
+ var eo = response.config.params.end_offset;
+
+ return so !== undefined && eo !== undefined ? [so, eo] : null;
+ }
+ return null;
+ },
+ request: [
+ // spectrogram
+ function (counters, data) {
+ return formatMediaUrl(data.responseData.data.available.image["png"].url, counters);
+ },
+ // mp3
+ function (counters, data) {
+ return formatMediaUrl(data.responseData.data.available.audio["mp3"].url, counters);
+ }
+ ],
+ progression: [
+ mediaProgressor, mediaProgressor
+ ],
+ count: 10,
+ method: "HEAD",
+ progressive: true
+ };
}
- ]
+ }
}
});
\ No newline at end of file
diff --git a/src/components/services/angularjsRailsResource.js b/src/components/services/angularjsRailsResource.js
index 356ccc66..bcd5fcfd 100644
--- a/src/components/services/angularjsRailsResource.js
+++ b/src/components/services/angularjsRailsResource.js
@@ -5,7 +5,8 @@
// MIT License
// https://github.com/tpodom/angularjs-rails-resource
-angular.module('rails', [])
+angular
+ .module('rails', [])
.constant('casingTransformers', (function () {
/**
* Old function worked via reference - deprecated
diff --git a/src/components/services/predictiveCache.js b/src/components/services/predictiveCache.js
index ae7491aa..08bf463e 100644
--- a/src/components/services/predictiveCache.js
+++ b/src/components/services/predictiveCache.js
@@ -6,18 +6,23 @@ angular
function () {
var _listeners = [];
return {
- listeners: function() {
+ listeners: function () {
return _listeners;
},
- request: function pciRequest(httpConfig) {
+ /*request: function pciRequest(httpConfig) {
- _listeners.forEach(function)
- return httpConfig;
+ },*/
+ response: function pciResponse(response) {
+
+ _listeners.forEach(function (listener) {
+ listener(response);
+ });
+
+ return response;
}
/*
requestError: ,
- response: ,
responseError:
*/
};
@@ -25,95 +30,311 @@ angular
])
.factory(
"predictiveCache",
- ["$http", "predictiveCacheInterceptor", function ($http, predictiveCacheInterceptor) {
+ [
+ "$http",
+ "predictiveCacheInterceptor",
+ "$q",
+ function ($http, predictiveCacheInterceptor, $q) {
- var defaults = {
- name: null,
- match: null,
- request: [],
- progression: [],
- count: 10,
- method: "HEAD"
- };
- var acceptableVerbs = ["GET", "HEAD", "POST", "PUT", "DELETE"];
- var defaultProgression = 1;
- var unnamedProfiles = 0;
+ var defaults = {
+ name: null,
+ /**
+ * Match can be a regex or a function.
+ * @param: {string} url
+ * @returns {null|Array} matches
+ */
+ match: null,
+ request: [],
+ progression: [],
+ count: 10,
+ method: "HEAD",
+ progressive: true
+ };
+ var acceptableVerbs = ["GET", "HEAD", "POST", "PUT", "DELETE"];
+ var defaultProgression = 1;
+ var unnamedProfiles = 0;
- var profiles = {};
+ var profiles = {};
- function validateProfile(settings) {
- settings = angular.extend({}, defaults, settings);
+ function validateProfile(settings) {
+ settings = angular.extend({}, defaults, settings);
- if (settings.name) {
- if (!angular.isString(settings.name)) {
- throw new Error("The provided name must be a string");
+ if (settings.name) {
+ if (!angular.isString(settings.name)) {
+ throw new Error("The provided name must be a string");
+ }
+ }
+ else {
+ unnamedProfiles++;
+ settings.name = "UnnamedProfile" + unnamedProfiles;
}
- }
- else {
- unnamedProfiles++;
- settings.name = "UnnamedProfile" + unnamedProfiles;
- }
- if (settings.match) {
- if (!(settings.match instanceof RegExp)) {
- throw new Error("The value for match must be a regular expression");
+ if (settings.match) {
+ if (!(settings.match instanceof RegExp) && !angular.isFunction(settings.match)) {
+ throw new Error("The value for match must be a regular expression or a function");
+ }
+ }
+ else {
+ throw new Error("A value for match must be provided");
}
- }
- else {
- throw new Error("A value for match must be provided");
- }
- if (angular.isArray(settings.request) && settings.request.length > 0) {
- var isStrings = settings.request.every(angular.isString);
- if (!isStrings) {
- throw new Error("requests must be an array of strings");
+ function isStringOrFunction(value) {
+ return angular.isString(value) || angular.isFunction(value);
}
+
+ if (angular.isArray(settings.request) && settings.request.length > 0) {
+ var isStringsOrFunctions = settings.request.every(isStringOrFunction);
+ if (!isStringsOrFunctions) {
+ throw new Error("requests must be an array of strings or functions");
+ }
+ }
+ else {
+ throw new Error("requests must be an array of strings or functions");
+ }
+
+ // http://stackoverflow.com/a/16046903
+ //var numGroups = (new RegExp(settings.match.toString() + '|')).exec('').length - 1;
+ var isArray = angular.isArray(settings.progression),
+ isEmpty = isArray && settings.progression.length === 0,
+ isNumber = angular.isNumber(settings.progression) && !isArray,
+ isNumberFunctionArray = isArray && settings.progression.every(function (value) {
+ return angular.isFunction(value) || angular.isNumber(value);
+ });
+ if (settings.progression === null || settings.progression === undefined) {
+ settings.progression = defaultProgression;
+ }
+ else if (isEmpty || !isNumber && !isNumberFunctionArray) {
+ throw new Error("progression must be an array of numbers/functions");
+ }
+
+ if (!angular.isNumber(settings.count) || settings.count < 0) {
+ throw new Error("count must be a positive integer");
+ }
+
+ if (acceptableVerbs.indexOf(settings.method) == -1) {
+ throw new Error("A valid http method is required");
+ }
+
+ if (settings.progressive !== true && settings.progressive !== false) {
+ throw new Error("progressive must be boolean");
+ }
+
+ return settings;
}
- else {
- throw new Error("requests must be an array of strings");
- }
- // http://stackoverflow.com/a/16046903
- //var numGroups = (new RegExp(settings.match.toString() + '|')).exec('').length - 1;
- var isArray = angular.isArray(settings.progression),
- isEmpty = isArray && settings.progression.length === 0,
- isNumber = angular.isNumber(settings.progression) && !isArray,
- isNumberFunctionArray = isArray && settings.progression.every(function (value) {
- return angular.isFunction(value) || angular.isNumber(value);
+ /**
+ * Create a chain of promises that will actually cache the resources
+ * @param profile
+ * @param url
+ * @param match
+ */
+ function createCacheRequests(profile, response, match) {
+ var url = response.config.url,
+ data = response.data;
+
+ // parse matched values from regex
+ var base = [],
+ previous = [];
+ for (var m = 0; m < match.length; m++) {
+ // try and convert match to number, if fail, leave as is
+ var num = Number(match[m]),
+ value = isNaN(num) ? match[m] : num;
+ base[m] = previous[m] = value;
+ }
+
+ // each capture group represents a parameter that may progress
+ var progressions = previous.map(function (value, index) {
+ var progression = defaultProgression;
+ if (profile.progression.length) {
+ progression = profile.progression[index];
+ }
+ else if (profile.progression) {
+ progression = profile.progression;
+ }
+
+ return progression;
+ });
+
+ function formatRequests(req, index) {
+ if (req.apply) {
+ return req.call(
+ undefined,
+ this,
+ {profile: profile, triggerUrl: url, responseData: data});
+ }
+ else {
+ return req.format(this);
+ }
+ }
+
+ function invokeHttp(url) {
+ console.debug("predictiveCache:promiseResolution enqueued " + url);
+ return $http({
+ url: url,
+ method: profile.method
+ }).then(function (value) {
+ console.debug("predictiveCache:promiseResolution completed " + url);
+ return value;
});
- if (settings.progression === null || settings.progression === undefined) {
- settings.progression = defaultProgression;
- }
- else if (isEmpty || !isNumber && !isNumberFunctionArray) {
- throw new Error("progression must be an array of numbers/functions");
- }
+ }
+
+ // count is the number of times to progress
+ var commands = [];
+ count:for (var i = 0; i < profile.count; i++) {
+
+ // apply each progression for each variable
+ var current = [];
+ for (var p = 0; p < progressions.length; p++) {
+
+ // get the number or function to increment by
+ var progression = progressions[p],
+ prev = previous[p];
+
+ if (progression.call) {
+ current[p] = progression.call(
+ undefined,
+ prev,
+ {count: i, profile: profile, triggerUrl: url, responseData: data});
+ }
+ else {
+ current[p] = prev + progression;
+ }
+
+ // break the count loop early if any of the progressions fail
+ if (current[p] === undefined) {
+ break count;
+ }
+ }
+
+ // apply the current values to the request strings
+ var currentCommands = profile.request.map(formatRequests, current);
+
+ Array.prototype.push.apply(commands, currentCommands);
+
+ // lastly, overwrite previous values for next loop
+ previous = current;
+ }
+
+ // finally ready to issue http requests!
+ var promises;
+ if (profile.progressive) {
+ // make requests consecutively, in a progressive fashion
+ // but batch each progression's requests into groups
+ var requestsPerProgression = profile.request.length;
+ promises = commands.reduce(function (promiseChain, current, index, array) {
+ if (index % requestsPerProgression === 0) {
+ var batchCommands = array.slice(index, index + requestsPerProgression);
+ if (batchCommands.length !== 0) {
+ return promiseChain.then(function () {
+ //console.debug("execute next http requests", index / requestsPerProgression, batchCommands);
+ if (batchCommands === 1) {
+ return invokeHttp(batchCommands[0]);
+ }
+ else {
+ return $q.all(batchCommands.map(invokeHttp));
+ }
+ });
+ }
+ else {
+ return promiseChain;
+ }
+ }
+ else {
+ return promiseChain;
+ }
+ }, $q.when());
+ }
+ else {
+ // just request everything at once
+ promises = $q.all(commands.map(invokeHttp));
+ }
- if (!angular.isNumber(settings.count) || settings.count < 0) {
- throw new Error("count must be a positive integer");
+ console.debug("predictiveCache:promiseResolution Promises enqueued");
+
+ return promises
+ .catch(function (error) {
+ console.error("predictiveCache:promiseResolution " + error, error);
+ })
+ .finally(function () {
+ console.debug("predictiveCache:promiseResolution complete");
+ });
}
- if (acceptableVerbs.indexOf(settings.method) == -1) {
- throw new Error("A valid http method is required");
+ /**
+ * Allows a function to simulate the API of a RegExp
+ * @param {Object} response - the url to match against
+ * @param {RegExp|function} matcher - the object that determines a match
+ * @returns {null|Array} - the matches if present
+ */
+ function isMatch(response, matcher) {
+ if (angular.isFunction(matcher)) {
+ var matches = matcher(response.config.url, response);
+ if (angular.isArray(matches) || matches === null) {
+ return matches;
+ }
+
+ throw new Error("The match function must conform to the RegExp.match API");
+ }
+ else if (matcher instanceof RegExp) {
+ var match = matcher.exec(response.config.url);
+ return match === null ? match : match.slice(1);
+ }
+ else {
+ throw new Error("The supplied matcher is neither a RegExp or a function!");
+ }
}
- return settings;
- }
+ /**
+ * Checks the url and returns a series of promises
+ * @returns {Promise}
+ * @param response
+ */
+ function checkProfiles(response) {
+ return $q(function (resolve, reject) {
+ // initially determine if the url matches any of the profiles
+ var matches = [];
+ angular.forEach(profiles, function (p, key) {
+ var match = isMatch(response, p.match);
+ if (match) {
+ // then trigger async resolution
+ matches.push(createCacheRequests(p, response, match));
+ }
+ });
- function createInterceptor() {
- };
+ if (matches.length > 0) {
+ resolve($q.all(matches));
+ }
+ else {
+ reject();
+ }
+ });
+ }
- return function predictiveCache(profile) {
- if (!angular.isObject(profile)) {
- throw new Error("A profile is required");
+ /**
+ * The callback from the http interceptor
+ * @param url - the url from the config.
+ */
+ function interceptorCallback(response) {
+ // knock it off to async ASAP so we don't block http stuff
+ checkProfiles(response);
}
- var settings = validateProfile(profile);
+ // setup callback
+ predictiveCacheInterceptor.listeners().push(interceptorCallback);
- profiles[settings.name] = settings;
+ // return initializer
+ return function predictiveCache(profile) {
+ if (!angular.isObject(profile)) {
+ throw new Error("A profile is required");
+ }
- return settings;
- };
- }])
- .config(['$httpProvider', function($httpProvider) {
+ var settings = validateProfile(profile);
+
+ profiles[settings.name] = settings;
+
+ return settings;
+ };
+ }])
+ .config(['$httpProvider', function ($httpProvider) {
$httpProvider.interceptors.push("predictiveCacheInterceptor");
}]);
\ No newline at end of file
diff --git a/src/components/services/predictiveCache.spec.js b/src/components/services/predictiveCache.spec.js
index f9b90baf..4dc9c7e1 100644
--- a/src/components/services/predictiveCache.spec.js
+++ b/src/components/services/predictiveCache.spec.js
@@ -1,25 +1,27 @@
describe("The predictiveCache service", function () {
- var predictiveCache,
- testProfile,
+ var testProfile,
$httpProvider;
beforeEach(module("bawApp.services", function (_$httpProvider_) {
$httpProvider = _$httpProvider_;
-
}));
- beforeEach(inject(function (_predictiveCache_) {
- predictiveCache = _predictiveCache_;
-
+ beforeEach(function () {
testProfile = {
name: "Media cache ahead",
match: /google\.com\?page=(.*)&size=(.*)/,
- request: ["one url", "another url"],
+ request: [
+ function (args, data) {
+ var a = args[0], b = args[1];
+ return "one " + a + " url" + b;
+ },
+ "another {0} url {1}"
+ ],
progression: [
30.0,
- function (data, previous) {
+ function (previous, data) {
var next = previous + 30.0;
- if (next >= data.max) {
+ if (data.responseData && next >= data.responseData.max) {
return;
}
else {
@@ -28,10 +30,10 @@ describe("The predictiveCache service", function () {
}
],
count: 10,
- method: "HEAD"
+ method: "HEAD",
+ progressive: false
};
-
- }));
+ });
describe("The predictive cache http interceptor", function () {
var predictiveCacheInterceptor, $httpBackend;
@@ -43,7 +45,7 @@ describe("The predictiveCache service", function () {
it("ensure the interceptor implements the expected methods", function () {
expect(predictiveCacheInterceptor).toImplement({
- request: null,
+ response: null,
listeners: null
});
});
@@ -53,82 +55,347 @@ describe("The predictiveCache service", function () {
expect(index >= 0).toBeTrue();
});
- it("ensures the request interceptor calls the correct method on every listener when a request is made", function () {
- predictiveCacheInterceptor.listeners().push();
- spyOn(predictiveCache, "interceptorCallback");
+ it("ensures the response interceptor calls the correct method on every listener when a request is made", function () {
- var mockHttpConfig = {
- url: "some fake url"
+ var listenerUrl = null, listenerDataSome = null;
+ predictiveCacheInterceptor.listeners().push(function (response) {
+ listenerUrl = response.config.url;
+ listenerDataSome = response.data.some;
+ });
+ var listenerUrl2 = null, listenerDataSome2 = null;
+ predictiveCacheInterceptor.listeners().push(function (response) {
+ listenerUrl2 = response.config.url;
+ listenerDataSome2 = response.data.some;
+ });
+
+ var mockHttpResponse = {
+ config: {
+ url: "some fake url"
+ },
+ data: {
+ some: "data"
+ }
};
- predictiveCacheInterceptor.request(mockHttpConfig);
+ predictiveCacheInterceptor.response(mockHttpResponse);
- expect(predictiveCache.interceptorCallback).toHaveBeenCalled();
- expect(predictiveCache.interceptorCallback).toHaveBeenCalledWith("some fake url");
+ expect(listenerUrl).toBe("some fake url");
+ expect(listenerUrl2).toBe("some fake url");
+ expect(listenerDataSome).toBe("data");
+ expect(listenerDataSome2).toBe("data");
});
- it("returns the http config unmodified", inject(["$http", function ($http) {
- spyOn(predictiveCacheInterceptor, "request").and.callThrough();
+ it("returns the response unmodified", inject(["$http", function ($http) {
+ spyOn(predictiveCacheInterceptor, "response").and.callThrough();
+
+ $httpBackend.expectGET("www.url.com").respond(200, {max: 30});
- $httpBackend.expectGET("www.url.com").respond(200);
- var reqConfig;
- $http.get("www.url.com").success(function(d,s,h,c,st) {
- reqConfig = c;
+ var response;
+ $http.get("www.url.com").success(function (d, s, h, c, st) {
+ response = {
+ data: d,
+ status: s,
+ headers: h,
+ config: c,
+ statusText: st || ""
+ };
});
$httpBackend.flush();
- expect(predictiveCacheInterceptor.request).toHaveBeenCalled();
- var interceptorInput = predictiveCacheInterceptor.request.calls.argsFor(0);
+ expect(predictiveCacheInterceptor.response).toHaveBeenCalled();
+ var interceptorInput = predictiveCacheInterceptor.response.calls.argsFor(0);
- var e = JSON.stringify(interceptorInput[0]),
- a = JSON.stringify(reqConfig);
+ var e = JSON.stringify(interceptorInput[0], undefined, 2),
+ a = JSON.stringify(response, undefined, 2);
expect(a).toBe(e);
}]));
});
describe("The functionality of the cacher", function () {
- var configuredProfile, $http,
- exampleUrl = "http://www.google.com?page=1&size=10";
+ var configuredProfile,
+ $http,
+ exampleUrl = "http://www.google.com?page=1&size=10",
+ httpEvents = [],
+ predictiveCache;
+
+ beforeEach(function () {
+ module(function ($provide) {
+ $provide.factory("unitTestInterceptor", function () {
+ return {
+ request: function (httpConfig) {
+ httpEvents.push("ENQUEUE: " + httpConfig.url);
+ return httpConfig;
+ },
+ response: function (response) {
+ httpEvents.push("COMPLETED: " + response.config.url);
+ return response;
+ }
+ };
+ });
- beforeEach(inject(function ($injector) {
- $httpBackend = $injector.get("$httpBackend");
+ $httpProvider.interceptors.push("unitTestInterceptor");
+ });
- // create .when for requests common to all tests
+ inject(function ($injector) {
+ $httpBackend = $injector.get("$httpBackend");
- // grab a reference to $http
- $http = $injector.get("$http");
+ // create .when for requests common to all tests
- // set up the cache service (register everything)
- configuredProfile = predictiveCache(testProfile);
- }));
+ // grab a reference to $http
+ $http = $injector.get("$http");
+
+ // set up the cache service (register everything)
+ predictiveCache = $injector.get("predictiveCache");
+ configuredProfile = predictiveCache(testProfile);
+ });
+
+ httpEvents = [];
+ });
afterEach(function () {
- $httpBackend.verifyNoOutstandingExpectation();
+ $httpBackend.verifyNoOutstandingExpectation(false);
$httpBackend.verifyNoOutstandingRequest();
});
+ function assertHttpEventsAreCorrect(expectedOrder) {
+ expect(httpEvents.length).toBe(expectedOrder.length);
+
+ httpEvents.forEach(function (actualEvent, index) {
+ expect(actualEvent).toBe(expectedOrder[index]);
+ });
+ }
+
it("makes the required requests when the url is matched", function () {
+ var expectedUrls = [
+ ["one 31 url40", "another 31 url 40"],
+ ["one 61 url70", "another 61 url 70"],
+ ["one 91 url100", "another 91 url 100"],
+ ["one 121 url130", "another 121 url 130"],
+ ["one 151 url160", "another 151 url 160"],
+ ["one 181 url190", "another 181 url 190"],
+ ["one 211 url220", "another 211 url 220"],
+ ["one 241 url250", "another 241 url 250"],
+ ["one 271 url280", "another 271 url 280"],
+ ["one 301 url310", "another 301 url 310"]
+ ];
+
+ var expectedOrder = [
+ "ENQUEUE: " + exampleUrl,
+ "COMPLETED: " + exampleUrl,
+ "ENQUEUE: one 31 url40",
+ "ENQUEUE: another 31 url 40",
+ "ENQUEUE: one 61 url70",
+ "ENQUEUE: another 61 url 70",
+ "ENQUEUE: one 91 url100",
+ "ENQUEUE: another 91 url 100",
+ "ENQUEUE: one 121 url130",
+ "ENQUEUE: another 121 url 130",
+ "ENQUEUE: one 151 url160",
+ "ENQUEUE: another 151 url 160",
+ "ENQUEUE: one 181 url190",
+ "ENQUEUE: another 181 url 190",
+ "ENQUEUE: one 211 url220",
+ "ENQUEUE: another 211 url 220",
+ "ENQUEUE: one 241 url250",
+ "ENQUEUE: another 241 url 250",
+ "ENQUEUE: one 271 url280",
+ "ENQUEUE: another 271 url 280",
+ "ENQUEUE: one 301 url310",
+ "ENQUEUE: another 301 url 310",
+ "COMPLETED: one 31 url40",
+ "COMPLETED: another 31 url 40",
+ "COMPLETED: one 61 url70",
+ "COMPLETED: another 61 url 70",
+ "COMPLETED: one 91 url100",
+ "COMPLETED: another 91 url 100",
+ "COMPLETED: one 121 url130",
+ "COMPLETED: another 121 url 130",
+ "COMPLETED: one 151 url160",
+ "COMPLETED: another 151 url 160",
+ "COMPLETED: one 181 url190",
+ "COMPLETED: another 181 url 190",
+ "COMPLETED: one 211 url220",
+ "COMPLETED: another 211 url 220",
+ "COMPLETED: one 241 url250",
+ "COMPLETED: another 241 url 250",
+ "COMPLETED: one 271 url280",
+ "COMPLETED: another 271 url 280",
+ "COMPLETED: one 301 url310",
+ "COMPLETED: another 301 url 310"
+ ];
$httpBackend.expectGET(exampleUrl).respond(200);
- $httpBackend.expectHEAD("one url").respond(200);
- $httpBackend.expectHEAD("another url").respond(200);
+ expectedUrls.forEach(function (pair) {
+ $httpBackend.expectHEAD(pair[0]).respond(200);
+ $httpBackend.expectHEAD(pair[1]).respond(200);
+ });
+
$http.get(exampleUrl);
$httpBackend.flush();
+
+ assertHttpEventsAreCorrect(expectedOrder);
});
- });
- it("registers a new http interceptor when a profile is added", function () {
- var interceptorCount = $httpProvider.interceptors.length;
+ it("correctly cancels an progression", function () {
+ var expectedUrls = [
+ ["one 31 url40", "another 31 url 40"],
+ ["one 61 url70", "another 61 url 70"],
+ ["one 91 url100", "another 91 url 100"]
+ ];
+
+ $httpBackend.expectGET(exampleUrl).respond(200, {max: 101});
+ expectedUrls.forEach(function (pair) {
+ $httpBackend.expectHEAD(pair[0]).respond(200);
+ $httpBackend.expectHEAD(pair[1]).respond(200);
+ });
+
+ $http.get(exampleUrl);
+
+ $httpBackend.flush();
+ });
+
+ it("correctly handles using match function", function () {
+ testProfile.match = function (url) {
+ if (/google\.com\?page=\d+&size=\d+/.test(url)) {
+ return [
+ /page=([\.\d]+)/.exec(url)[1],
+ /size=([\.\d]+)/.exec(url)[1]
+ ];
+ }
+ return null;
+ };
+ configuredProfile = predictiveCache(testProfile);
+ var expectedUrls = [
+ ["one 31 url40", "another 31 url 40"],
+ ["one 61 url70", "another 61 url 70"],
+ ["one 91 url100", "another 91 url 100"]
+ ];
+
+ $httpBackend.expectGET(exampleUrl).respond(200, {max: 101});
+ expectedUrls.forEach(function (pair) {
+ $httpBackend.expectHEAD(pair[0]).respond(200);
+ $httpBackend.expectHEAD(pair[1]).respond(200);
+ });
+
+ $http.get(exampleUrl);
+
+ $httpBackend.flush();
+ });
+
+ it("correctly fails when a match function does not confirm to the RegExp API", function () {
+ testProfile.match = function (url) {
+ return;
+ };
+ testProfile.progressions = [
+ function() {},
+ function() {}
+ ];
+
+ expect(function () {
+ configuredProfile = predictiveCache(testProfile);
+ $httpBackend.expectGET(exampleUrl).respond(200);
+ $http.get(exampleUrl);
- var profile = predictiveCache(testProfile);
+ $httpBackend.flush(1);
+ }).toThrowError("The match function must conform to the RegExp.match API");
+ });
- expect($httpProvider.interceptors.length).toBe(interceptorCount + 1);
+ it("correctly batches requests as a series of chained promised when progressive===true", function () {
+ testProfile.progressive = true;
+ configuredProfile = predictiveCache(testProfile);
+ var expectedUrls = [
+ ["one 31 url40", "another 31 url 40"],
+ ["one 61 url70", "another 61 url 70"],
+ ["one 91 url100", "another 91 url 100"],
+ ["one 121 url130", "another 121 url 130"],
+ ["one 151 url160", "another 151 url 160"],
+ ["one 181 url190", "another 181 url 190"],
+ ["one 211 url220", "another 211 url 220"],
+ ["one 241 url250", "another 241 url 250"],
+ ["one 271 url280", "another 271 url 280"],
+ ["one 301 url310", "another 301 url 310"]
+ ];
+
+ var expectedOrder = [
+ "ENQUEUE: " + exampleUrl,
+ "COMPLETED: " + exampleUrl,
+ "ENQUEUE: one 31 url40", "ENQUEUE: another 31 url 40", "COMPLETED: one 31 url40", "COMPLETED: another 31 url 40",
+ "ENQUEUE: one 61 url70", "ENQUEUE: another 61 url 70", "COMPLETED: one 61 url70", "COMPLETED: another 61 url 70",
+ "ENQUEUE: one 91 url100", "ENQUEUE: another 91 url 100", "COMPLETED: one 91 url100", "COMPLETED: another 91 url 100",
+ "ENQUEUE: one 121 url130", "ENQUEUE: another 121 url 130", "COMPLETED: one 121 url130", "COMPLETED: another 121 url 130",
+ "ENQUEUE: one 151 url160", "ENQUEUE: another 151 url 160", "COMPLETED: one 151 url160", "COMPLETED: another 151 url 160",
+ "ENQUEUE: one 181 url190", "ENQUEUE: another 181 url 190", "COMPLETED: one 181 url190", "COMPLETED: another 181 url 190",
+ "ENQUEUE: one 211 url220", "ENQUEUE: another 211 url 220", "COMPLETED: one 211 url220", "COMPLETED: another 211 url 220",
+ "ENQUEUE: one 241 url250", "ENQUEUE: another 241 url 250", "COMPLETED: one 241 url250", "COMPLETED: another 241 url 250",
+ "ENQUEUE: one 271 url280", "ENQUEUE: another 271 url 280", "COMPLETED: one 271 url280", "COMPLETED: another 271 url 280",
+ "ENQUEUE: one 301 url310", "ENQUEUE: another 301 url 310", "COMPLETED: one 301 url310", "COMPLETED: another 301 url 310"
+ ];
+ $httpBackend.expectGET(exampleUrl).respond(200);
+ $http.get(exampleUrl);
+
+ expectedUrls.forEach(function (pair, index) {
+ // expect two requests at a time to be issued
+ // two per progression
+ // so expect only two every flush!
+ //console.debug("expectations!", pair[0], pair[1]);
+ $httpBackend.expectHEAD(pair[0]).respond(200);
+ $httpBackend.expectHEAD(pair[1]).respond(200);
+ });
+
+ //console.debug("begin execution");
+ //console.debug("flush!");
+
+ // promise resolution means the next all the requests are processed in a single
+ // flush whether we like it or not.
+ $httpBackend.flush();
+ //console.debug("flush done!");
+
+ assertHttpEventsAreCorrect(expectedOrder);
+ });
+
+ it("handles negative/odd progressions", function () {
+ testProfile.progression = [
+ function (previous, data) {
+ return Math.pow(previous, 2) - Math.pow(5, 2);
+ },
+ -10
+ ];
+ testProfile.request = ["hello {0} {1}"];
+ configuredProfile = predictiveCache(testProfile);
+ var expectedUrls = [
+ ["hello -24 0"],
+ ["hello 551 -10"],
+ ["hello 303576 -20"],
+ ["hello 92158387751 -30"],
+ ["hello 8.493168432863667e+21 -40"],
+ ["hello 7.213391002899188e+43 -50"],
+ ["hello 5.203300976070695e+87 -60"],
+ ["hello 2.7074341047578252e+175 -70"],
+ ["hello Infinity -80"],
+ ["hello Infinity -90"]
+ ];
+
+ $httpBackend.expectGET(exampleUrl).respond(200);
+ expectedUrls.forEach(function (pair) {
+ $httpBackend.expectHEAD(pair[0]).respond(200);
+ });
+
+ $http.get(exampleUrl);
+
+ $httpBackend.flush();
+ });
});
describe("Validating the input profile", function () {
+ var predictiveCache;
+ beforeEach(inject(function (_predictiveCache_) {
+ predictiveCache = _predictiveCache_;
+ }));
+
+
it("requires an object to function", function () {
expect(function () {
predictiveCache();
@@ -192,15 +459,21 @@ describe("The predictiveCache service", function () {
}).toThrowError("A value for match must be provided");
});
- it("will fail if the supplied match regular expression is not a RegExp", function () {
- testProfile.match = "test";
+ it("will fail if the supplied match regular expression is not a RegExp or a function", function () {
+ testProfile.match = function () {
+ };
+ var profile = predictiveCache(testProfile);
+
+ testProfile.match = /test/;
+ profile = predictiveCache(testProfile);
+ testProfile.match = "test";
expect(function () {
predictiveCache(testProfile);
- }).toThrowError("The value for match must be a regular expression");
+ }).toThrowError("The value for match must be a regular expression or a function");
});
- it("ensures the request value is an array of strings", function () {
+ it("ensures the request value is an array of strings or functions", function () {
var failValues = [null, [], 33, [33]];
failValues.forEach(function (item) {
@@ -208,10 +481,24 @@ describe("The predictiveCache service", function () {
expect(function () {
predictiveCache(testProfile);
- }).toThrowError("requests must be an array of strings");
+ }).toThrowError("requests must be an array of strings or functions");
});
});
+ [
+ {key: "an array of strings", in: ["hello", "world"]},
+ {key: "an array of strings and functions", in: [angular.noop, "hello world"]},
+ {key: "an array of functions", in: [angular.noop, angular.noop]}
+ ].forEach(function (requestTest) {
+ it("allows the request value to be " + requestTest.key, function () {
+ testProfile.request = requestTest.in;
+
+ var profile = predictiveCache(testProfile);
+ expect(profile.request).toBe(requestTest.in);
+ });
+ });
+
+
[
{key: "null", in: null, out: 1},
{key: "undefined", in: undefined, out: 1},
@@ -268,5 +555,24 @@ describe("The predictiveCache service", function () {
expect(profile.count).toBe(10);
});
+
+ it("ensures the progressive option strictly accepts booleans", function () {
+ testProfile.progressive = true;
+ var profile = predictiveCache(testProfile);
+
+ testProfile.progressive = false;
+ profile = predictiveCache(testProfile);
+
+ delete testProfile.progressive;
+ profile = predictiveCache(testProfile);
+ expect(profile.progressive).toBeTrue();
+
+ [null, 0, "false", [], {}, [false]].forEach(function (errorValue) {
+ expect(function () {
+ testProfile.progressive = errorValue;
+ predictiveCache(testProfile);
+ }).toThrowError("progressive must be boolean");
+ });
+ });
});
});
\ No newline at end of file