blob: d3b1d41192762c3524760205f60c40a33cc1ace2 [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
var mercury = require('mercury');
var _ = require('lodash');
var EventEmitter = require('events').EventEmitter;
var arraySet = require('../../lib/array-set');
var freeze = require('../../lib/mercury/freeze');
var sortedPush = require('../../lib/mercury/sorted-push-array');
var namespaceService = require('../namespace/service');
var smartService = require('../smart/service');
var log = require('../../lib/log')('services:recommendations:service');
var LEARNER_KEY = 'learner-shortcut';
var MAX_NUM_RECOMMENDATIONS = 10;
module.exports = {
getAll: getAll,
getRecommendationScore: getRecommendationScore,
setRecommendationScore: setRecommendationScore
};
// Singleton state for all the bookmarks.
var recommendationsObs = mercury.array([]);
smartService.loadOrCreate(
LEARNER_KEY,
smartService.constants.LEARNER_SHORTCUT, {
k: MAX_NUM_RECOMMENDATIONS
}
).catch(function(err) {
log.error(err);
});
/*
* Gets all the namespace items that are recommended based on our learning agent
* As new recommendations become available/removed the observable array will
* change to reflect the changes.
*
* The observable result has an events property which is an EventEmitter
* and emits 'end', 'itemError' events.
*
* @return {Promise.<mercury.array>} Promise of an observable array
* of recommended items
*/
function getAll() {
// Empty out the array
recommendationsObs.splice(0, recommendationsObs.getLength());
var immutableRecommendationsObs = freeze(recommendationsObs);
immutableRecommendationsObs.events = new EventEmitter();
return smartService.predict(LEARNER_KEY).then(getRecommendationItems);
function getRecommendationItems(recs) {
var allItems = recs.map(function(rec) {
var name = rec.item;
return addNamespaceItem(name).catch(function(err) {
immutableRecommendationsObs.events.emit('itemError', {
name: name,
error: err
});
log.error('Failed to create item for "' + name + '"', err);
});
});
Promise.all(allItems).then(function() {
immutableRecommendationsObs.events.emit('end');
}).catch(function() {
immutableRecommendationsObs.events.emit('end');
});
return immutableRecommendationsObs;
}
}
/*
* Gets the namespace items for a name and adds it to the observable array
*/
function addNamespaceItem(name) {
return namespaceService.getNamespaceItem(name)
.then(function(item) {
var sorter = 'objectName';
sortedPush(recommendationsObs, item, sorter);
});
}
/*
* Get the score of a particular recommended object name.
*/
function getRecommendationScore(name) {
return smartService.predict(LEARNER_KEY, {
name: name,
penalize: false
}).then(function(res) {
var match = res.filter(function(rec) {
return rec.item === name;
})[0];
return match ? match.score : 0;
});
}
/*
* Set the score of a particular object name.
* Note: This will penalize (or boost) parents of the given name.
*/
function setRecommendationScore(name, newScore) {
if (newScore > 0) {
addNamespaceItem(name);
} else {
arraySet.set(recommendationsObs, null, false, indexOf.bind(null, name));
}
return getRecommendationScore(name).then(function(curScore) {
var delta = newScore - curScore;
return smartService.update(LEARNER_KEY, {
name: name,
weight: delta
}).then(function() {
return curScore;
});
}).catch(function(err) {
log.error('Failed to set the recommendation score of', name, 'to', newScore,
err);
});
}
/*
* Check the observe array for the index of the given item. -1 if not present.
*/
function indexOf(name) {
return _.findIndex(recommendationsObs(), function(rec) {
// Since recommendations can be assigned out of order, check for undefined.
return rec !== undefined && name === rec.objectName;
});
}