Compiling a result through a chain of promises (JS inheritance/interfaces) - javascript

I'm building a "storage provider" that allows consuming code to store stuff through an interface. Consider the below code snippets to be pseudocode as I'm going for MCVE. I'm trying to get my hands on IMPORTANTDATA and IMPORTANTKEY below.
At the lowest level, I have a baseService:
define([], function(){
return function(){
this.sendRequest = function(data){
return $.ajax(data).done(function(response){
return response.IMPORTANTDATA; // <---- This is needed
}).fail(function(response){
throw new Error(response);
});
}
}
})
I build services with this to reuse some base functionality, for example - eventService:
define(["baseService"], function(baseService){
const eventService = new baseService();
eventService.postMediaEvent = function(eventType, mediaPath, storageProvider){
// isolated logic here
return eventService.sendRequest(someData);
}
})
This is where things start to get tricky: I have a baseStorageClient:
define(["eventService"], function (eventService) {
return function(){
this.storageProvider = null;
const self = this;
this.storeMetadata = function(eventType, mediaPath){
return eventService.postMediaEvent(eventType, mediaPath, self.storageProvider);
};
this.storeMedia = function(){
throw new Error("Not Implemented");
};
}
}
But this guy isn't ever used directly. I have instances of this created - for example, indexedDbClient:
define(["baseStorageClient"], function(baseStorageClient){
const indexedDbClient = new baseStorageClient();
indexedDbClient.storeMedia = function(blob){
return openDatabase().then(function () {
const request = database.transaction(storeName, "readwrite")
.objectStore(storeName)
.add(dbEntry);
request.onsuccess = function (event) {
logger.log("combined segments saved into database.");
// todo - figure out how to resolve here
return {
IMPORTANTKEY: dbEntry.mediaId // <---- This too
}
};
request.onerror = function (event) {
// todo: figure out how to reject here
logger.log("Unable to save segments " + e);
};
});
}
})
And this client is used within my storageInterface:
define(["indexedDbClient"], function(indexedDbClient){
const storageInterface = {};
var currentClient = indexedDbClient; // might be other clients
storageInterface.storeMedia = function (blob) {
return currentClient.storeMedia(blob).then(function(mediaPath) {
return currentClient.storeMetadata(eventType, mediaPath);
});
}
});
This is where things get super hairy. What I'm trying to achieve is the following:
storageInterface.storeMedia(superBuffer).then(function (importantStuff) {
// this should go storeMedia > baseStorageClient > eventService
importantStuff.IMPORTANTKEY;
importantStuff.IMPORTANTDATA;
});
But I can't quite figure out how to get this handled. How can I compile a result along a chain of promises like this?

There's two major problems:
You should treat done and fail as deprecated. They don't allow for any chaining, they will discard the results of the callback. Always use then.
sendRequest = function(data){
return $.ajax(data).then(function(response){
return response.IMPORTANTDATA;
}, function(response) {
throw new Error(response);
});
}
Your transaction doesn't return any promise yet, so there's nothing for you to chain onto. You'll need to promisify it first:
function promiseFromRequest(req) {
return new Promise(function(resolve, reject) {
req.onsuccess = resolve;
req.onerror = reject;
});
}
Now you can actually use it like so:
storeMedia = function(blob){
return openDatabase().then(function () {
return promiseFromRequest(database.transaction(storeName, "readwrite")
.objectStore(storeName)
.add(dbEntry))
.then(function (event) {
logger.log("combined segments saved into database.");
return {
IMPORTANTKEY: dbEntry.mediaId
}
}, function (e) {
logger.log("Unable to save segments " + e);
throw e;
};
});
};
With those, you should be able to combine the results from storeMedia and storeMetaData in some way.

Related

Using promise to work with web worker inside a JavaScript closure

I was executing an image processing operation in JavaScript which was working as expected expect one thing that sometimes it was freezing the UI, which made me to use Web worker to excute the image processing functions.
I have a scenario where i need to process multiple. Below is a summary of workflow which i am using to achieve the above feat.
//closure
var filter = (function(){
function process(args){
var promise = new Promise(function (resolve, reject) {
if (typeof (Worker) !== "undefined") {
if (typeof (imgWorker) == "undefined") {
imgWorker = new Worker("/processWorker.js");
}
imgWorker.postMessage(args);
imgWorker.onmessage = function (event) {
resolve(event.data);
};
} else {
reject("Sorry, your browser does not support Web Workers...");
}
});
return promise;
}
return {
process: function(args){
return process(args);
}
}
})();
function manipulate(args, callback){
filter.process(args).then(function(res){
callback(res);
});
}
Here, i am loading multiple images and passing them inside manipulate function.
The issue i am facing here in this scenario is that sometimes for few images Promise is not never resolved.
After debugging my code i figured out that it is because i am creating a Promise for an image while previous Promise was not resolved.
I need suggestions on how can i fix this issue, also i have another query should i use same closure(filter here in above scenario) multiple times or create new closure each time when required as below:
var filter = function(){
....
return function(){}
....
}
function manipulate(args, callback){
var abc = filter();
abc.process(args).then(function(res){
callback(res);
});
}
I hope my problem is clear, if not please comment.
A better approach would be to load your image processing Worker once only. during the start of your application or when it is needed.
After that, you can create a Promise only for the function you wish to call from the worker. In your case, filter can return a new Promise object every time that you post to the Worker. This promise object should only be resolved when a reply is received from the worker for the specific function call.
What is happening with your code is that, your promises are resolving even though the onmessage handler is handling a different message from the Worker. ie. if you post 2 times to the worker. if the second post returns a message it automatically resolves both of your promise objects.
I created a worker encapsulation here Orc.js. Although it may not work out of the box due to the fact i haven't cleaned it of some dependencies i built into it. Feel free to use the methods i applied.
Additional:
You will need to map your post and onmessage to your promises. this will require you to modify your Worker code as well.
//
let generateID = function(args){
//generate an ID from your args. or find a unique way to distinguish your promises.
return id;
}
let promises = {}
// you can add this object to your filter object if you like. but i placed it here temporarily
//closure
var filter = (function(){
function process(args){
let id = generateID(args)
promises[id] = {}
promises[id].promise = new Promise(function (resolve, reject) {
if (typeof (Worker) !== "undefined") {
if (typeof (imgWorker) == "undefined") {
imgWorker = new Worker("/processWorker.js");
imgWorker.onmessage = function (event) {
let id = generateID(event.data.args) //let your worker return the args so you can check the id of the promise you created.
// resolve only the promise that you need to resolve
promises[id].resolve(event.data);
}
// you dont need to keep assigning a function to the onmessage.
}
imgWorker.postMessage(args);
// you can save all relevant things in your object.
promises[id].resolve = resolve
promises[id].reject = reject
promises[id].args = args
} else {
reject("Sorry, your browser does not support Web Workers...");
}
});
//return the relevant promise
return promises[id].promise;
}
return {
process: function(args){
return process(args);
}
}
})();
function manipulate(args, callback){
filter.process(args).then(function(res){
callback(res);
});
}
typescirpt equivalent on gist:
Combining answers from "Webworker without external files"
you can add functions to worker scope like the line `(${sanitizeThis.toString()})(this);,` inside Blob constructing array.
There are some problems regarding resolving promise outside of the promise enclosure, mainly about error catching and stack traces, I didn't bother because it works perfectly fine for me right now.
// https://stackoverflow.com/a/37154736/3142238
function sanitizeThis(self){
// #ts-ignore
// console.assert(this === self, "this is not self", this, self);
// 'this' is undefined
"use strict";
var current = self;
var keepProperties = [
// Required
'Object', 'Function', 'Infinity', 'NaN',
'undefined', 'caches', 'TEMPORARY', 'PERSISTENT',
"addEventListener", "onmessage",
// Optional, but trivial to get back
'Array', 'Boolean', 'Number', 'String', 'Symbol',
// Optional
'Map', 'Math', 'Set',
"console",
];
do{
Object.getOwnPropertyNames(
current
).forEach(function(name){
if(keepProperties.indexOf(name) === -1){
delete current[name];
}
});
current = Object.getPrototypeOf(current);
} while(current !== Object.prototype);
}
/*
https://hacks.mozilla.org/2015/07/how-fast-are-web-workers/
https://developers.google.com/protocol-buffers/docs/overview
*/
class WorkerWrapper
{
worker;
stored_resolves = new Map();
constructor(func){
let blob = new Blob([
`"use strict";`,
"const _postMessage = postMessage;",
`(${sanitizeThis.toString()})(this);`,
`const func = ${func.toString()};`,
"(", function(){
// self.onmessage = (e) => {
addEventListener("message", (e) => {
_postMessage({
id: e.data.id,
data: func(e.data.data)
});
})
}.toString(), ")()"
], {
type: "application/javascript"
});
let url = URL.createObjectURL(blob);
this.worker = new Worker(url);
URL.revokeObjectURL(url);
this.worker.onmessage = (e) => {
let { id, data } = e.data;
let resolve = this.stored_resolves.get(id);
this.stored_resolves.delete(id);
if(resolve){
resolve(data);
} else{
console.error("invalid id in message returned by worker")
}
}
}
terminate(){
this.worker.terminate();
}
count = 0;
postMessage(arg){
let id = ++this.count;
return new Promise((res, rej) => {
this.stored_resolves.set(id, res);
this.worker.postMessage({
id,
data: arg
});
})
}
}
// usage
let worker = new WorkerWrapper(
(d) => { return d + d; }
);
worker.postMessage("HEY").then((e) => {
console.log(e); // HEYHEY
})
worker.postMessage("HELLO WORLD").then((f) => {
console.log(f); // HELLO WORLDHELLO WORLD
})
let worker2 = new WorkerWrapper(
(abc) => {
// you can insert anything here,
// just be aware of whether variables/functions are in scope or not
return(
{
"HEY": abc,
[abc]: "HELLO WORLD" // this particular line will fail with babel
// error "ReferenceError: _defineProperty is not defined",
}
);
}
);
worker2.postMessage("HELLO WORLD").then((f) => {
console.log(f);
/*
{
"HEY": "HELLO WORLD",
"HELLO WORLD": "HELLO WORLD"
}
*/
})
/*
observe how the output maybe out of order because
web worker is true async
*/

Return ajax response via JS Module pattern

UPDATE:
I decided that using the JS Module Pattern was not "keeping it simple", so I scrapped it and used jQuery's deferred object to return the data I was looking for. What I really needed was to simply load a JSON file and populate an object. I was just trying to be too fancy by incorporating the JS Module Pattern.
Many thanks to #kiramishima for the correct answer.
Below is the finished code:
function getData(){
var url = CONTEXT + "/json/myJsonFile.json";
return $.getJSON(url);
}
getData()
.done(function(data){
myGlobalObj = data;
})
.fail(function(data){
console.log("fetching JSON file failed");
});
I think I'm getting a little too fancy for my own good here. I'm loading a JSON file and trying to return the API via JS module pattern. Problem is that I believe I'm not implementing the promise correctly and I don't know how to fix it.
Here's my JSON:
{
"result": {
"one": {
"first_key":"adda",
"second_key":"beeb",
"third_key":"cffc"
},
"two": {
"first_key":"adda",
"second_key":"beeb",
"third_key":"cffc"
}
}
}
And here's my JS Module implementation:
var data = (function() {
var url = "/json/dummy.json";
var getAllData = function() {
return $.getJSON(url, function(result){});
};
var promise = getAllData(); // the promise
return {
getFirstObjSecondKey:function() {
return promise.success(function(data) {
return data.result.one.second_key;
});
},
getSecondObjThirdKey:function() {
return promise.success(function(data) {
return data.result.two.third_key;
});
},
};
})();
The problem is that "getAllData()" is coming back as undefined and I'm not sure why; that method returns a Promise that I should be able to handle in the "done" function. How far off am I?
Thanks for any helpful input. This is the first time I'm messing with the JS Module Pattern.
I dont know what is your problem, but I test with:
var getAllData = function() {
return $.getJSON('/json/dummy.json', function(result){})
}
getAllData().done(function(data){ console.log(data.result.one.second_key) }) // prints beeb
works fine in that case, but if try this:
var data = (function() {
var url = '/json/dummy.json';
var getAllData = function() {
return $.getJSON(url, function(result){});
};
return {
getFirstObjSecondKey:function() {
getAllData().done(function(data) {
return data.login;
});
},
getSecondObjThirdKey:function() {
getAllData().done(function(data) {
return data.name;
});
},
};
})();
data.getFirstObjSecondKey returns undefined, then can u pass anonymous function:
var data = (function() {
var url = '/json/dummy.json';
var getAllData = function() {
return $.getJSON(url, function(result){});
};
return {
getFirstObjSecondKey:function(callback) {
getAllData().done(function(data) {
callback(data.result.one.second_key);
});
},
getSecondObjThirdKey:function(callback) {
getAllData().done(function(data) {
callback(data.result.two.third_key);
});
},
};
})();
var t;
data.getFirstObjSecondKey(function(data){
//data should contain the object fetched by getJSON
console.log(data); // prints beeb
t = data; // assign t
})
console.log(t) // prints beeb
Other solution, return always the deferred object
kiramishima's answer works, but it mixes callbacks with Promises. If you're using promises, you should try not to mix both styles.
You have to return a Promise from your functions. Remember that promises can be chained, that is, if you return a Promise from the done function, that becomes the new Promise
var data = (function() {
var url = "/json/dummy.json";
var getAllData = function() {
return $.getJSON(url, function(result){});
};
return {
getFirstObjSecondKey:function() {
return getAllData().done(function(data) {
return new Promise(function(resolve, reject){
resolve(data.result.one.second_key);
});
});
},
getSecondObjThirdKey:function() {
return getAllData().done(function(data) {
return new Promise(function(resolve, reject){
resolve(data.result.one.third_key);
});
});
},
};
})();
data.getFirstObjSecondKey().done(function(secondKey) {
console.log('Second key', secondKey);
});

Combining two promises

I am really new to JavaScript and promises and to be honest I don't fully understand how promises work so I need some help.
I am using Google Cloud Messaging to push notifications from my site to my users. When users receive a notification and clicks on it, it opens a URL stored in a IndexedDB.
importScripts('IndexDBWrapper.js');
var KEY_VALUE_STORE_NAME = 'key-value-store', idb;
function getIdb() {
if (!idb) {
idb = new IndexDBWrapper(KEY_VALUE_STORE_NAME, 1, function (db) {
db.createObjectStore(KEY_VALUE_STORE_NAME);
});
}
return idb;
}
self.addEventListener('notificationclick', function (event) {
console.log('On notification click: ', event);
event.notification.close();
event.waitUntil(getIdb().get(KEY_VALUE_STORE_NAME, event.notification.tag).then(function (url) {
var redirectUrl = '/';
if (url) redirectUrl = url;
return clients.openWindow(redirectUrl);
}));
});
So in the code above, I know that the getIdb()...then() is a promise, but is the event.waitUntil also a promise?
The problem with the above code is that it opens a instance of Chrome every time the notification is clicked and I would prefer that it would utilize an existing instance if available. The following does just that:
self.addEventListener('notificationclick', function(event) {
console.log('On notification click: ', event.notification.tag);
event.notification.close();
event.waitUntil(
clients.matchAll({
type: "window"
})
.then(function(clientList) {
for (var i = 0; i < clientList.length; i++) {
var client = clientList[i];
if (client.url == '/' && 'focus' in client)
return client.focus();
}
if (clients.openWindow) {
return clients.openWindow('/');
}
})
);
});
However, now I have two promises, getIdb and clients.matchAll and I really have no idea how to combine the two promises and the two sets of code. Any help would be greatly appreciated. Thanks!
For reference, here is IndexDBWrapper.js:
'use strict';
function promisifyRequest(obj) {
return new Promise(function(resolve, reject) {
function onsuccess(event) {
resolve(obj.result);
unlisten();
}
function onerror(event) {
reject(obj.error);
unlisten();
}
function unlisten() {
obj.removeEventListener('complete', onsuccess);
obj.removeEventListener('success', onsuccess);
obj.removeEventListener('error', onerror);
obj.removeEventListener('abort', onerror);
}
obj.addEventListener('complete', onsuccess);
obj.addEventListener('success', onsuccess);
obj.addEventListener('error', onerror);
obj.addEventListener('abort', onerror);
});
}
function IndexDBWrapper(name, version, upgradeCallback) {
var request = indexedDB.open(name, version);
this.ready = promisifyRequest(request);
request.onupgradeneeded = function(event) {
upgradeCallback(request.result, event.oldVersion);
};
}
IndexDBWrapper.supported = 'indexedDB' in self;
var IndexDBWrapperProto = IndexDBWrapper.prototype;
IndexDBWrapperProto.transaction = function(stores, modeOrCallback, callback) {
return this.ready.then(function(db) {
var mode = 'readonly';
if (modeOrCallback.apply) {
callback = modeOrCallback;
}
else if (modeOrCallback) {
mode = modeOrCallback;
}
var tx = db.transaction(stores, mode);
var val = callback(tx, db);
var promise = promisifyRequest(tx);
var readPromise;
if (!val) {
return promise;
}
if (val[0] && 'result' in val[0]) {
readPromise = Promise.all(val.map(promisifyRequest));
}
else {
readPromise = promisifyRequest(val);
}
return promise.then(function() {
return readPromise;
});
});
};
IndexDBWrapperProto.get = function(store, key) {
return this.transaction(store, function(tx) {
return tx.objectStore(store).get(key);
});
};
IndexDBWrapperProto.put = function(store, key, value) {
return this.transaction(store, 'readwrite', function(tx) {
tx.objectStore(store).put(value, key);
});
};
IndexDBWrapperProto.delete = function(store, key) {
return this.transaction(store, 'readwrite', function(tx) {
tx.objectStore(store).delete(key);
});
};
One way to deal with multiple promises is with Promise.all
Promise.all([promise0, promise1, promise2]).then(function(valArray) {
// valArray[0] is result of promise0
// valArray[1] is result of promise1
// valArray[2] is result of promise2
});
read about promise.all - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all
event.waitUntil() takes a promise - this allows the browser to keep your worker alive until you've finished what you want to do (i.e. until the promise that you gave to event.waitUntil() has resolved).
As the other answer suggests, you can use Promise.all() within event.waitUntil. Promise.all() takes an array of promises and returns a promise, so you can call then on it. Your handling function will get an array of promise results when all of the promises you've provided to Promise.all have resolved. Your code will then look something like this (I haven't actually tested this, but it should be close):
self.addEventListener('notificationclick', function (event) {
event.notification.close();
event.waitUntil(Promise.all([
getIdb().get(KEY_VALUE_STORE_NAME, event.notification.tag),
clients.matchAll({ type: "window" })
]).then(function (resultArray) {
var url = resultArray[0] || "/";
var clientList = resultArray[1];
for (var i = 0; i < clientList.length; i++) {
var client = clientList[i];
if (client.url == '/' && 'focus' in client)
return client.focus();
}
if (clients.openWindow) {
return clients.openWindow(url);
}
}));
});
Though I am very late to the party but,
I think you can use this very light, vanilla JS library that does things for you. https://www.npmjs.com/package/easy-promise-all
Here is the small code sample for it.
var { EasyPromiseAll } = require('easy-promise-all');
EasyPromiseAll({
resultFromPromise1: Promise.resolve('first'),
resultFromPromise2: Promise.reject('second'),
}).then((results) => {
const { resultFromPromise1, resultFromPromise2 } = results;
console.log(resultFromPromise1, resultFromPromise2);
});

Nodejs - SolrClient, how to wait for response

I got a question regarding the solr-client module of nodejs. I'm using this module for querying against a solr-index.
The module itself works fine as long as I don't have to wait for finishing of the query and as long I need the result only as a async result.
But currently I cannot find out, how I will be able to await the finishing of a search request and use the result in a sequential way.
I have the follwing method in my manager
SolrManager.prototype.promisedQuery = function(query, callback) {
var solrClient = solr.createClient(this.configuration.cores.page);
var docs = null;
var finished = false;
var deferred = Q.defer();
var request = solrClient.search(query, function(err,obj){
if (!err) {
if (obj.response.numFound > 0) {
deferred.resolve(obj.response.docs);
} else {
deferred.resolve(null);
}
} else {
deferred.reject(err);
}
});
var records = null;
var promise = deferred.promise;
promise.then(function(result) {
records = result;
}).fail(function(error){
records = error;
});
return records;
};
The problem here is, that I try to wait for the result of the query and use it as return value of "promisedQuery".
I try since days to use this method in a sequential call, also with different additional modules like "wait.for", "q", etc. but nothing seems to work.
The callback function of the solr-client will always be executed after the manager-method has already returned. Also the promise-methods will be even called after the return from the manager-method.
Can someone help me out on that topic or have some tips, how I can await the response of the solr-client-search operation and then give it back in a sequential way?
Thanks for any help.
Udo Gerhards
over one week, it seems now that I have found a solution:
SolrManager.prototype.promisedQuery = function(query, callback) {
var solrClient = solr.createClient(this.configuration.cores.page);
var docs = null;
var deferred = Q.defer();
var request = solrClient.search(query, function(err,obj){
if (!err) {
if (obj.response.numFound > 0) {
deferred.resolve(obj.response.docs);
} else {
deferred.resolve(null);
}
} else {
deferred.reject(err);
}
});
return deferred.promise;
};
in all other managers, which are calling the above function:
...
var dbPromise = this.solrManager.promisedQuery(query);
var _self = this;
return Q.async(function*(){
var result = yield dbPromise;
return result;
});
...
After first tests, it seems that synchronized methods will wait until the promise is settled.
The only thing is, that it runs only with NodeJs version 0.11.10, which supports generator functions, with activated --harmony-flag and "q"-module.
Best regards
Udo
You are just using the promises a bit incorrectly. Instead of returning records, you need to return 'deferred.promise'. It should look something like this (note that you don't need the callback you passed into promisedQuery).
SolrManager.prototype.promisedQuery = function(query) {
var solrClient = solr.createClient(this.configuration.cores.page),
deferred = Q.defer();
solrClient.search(query, function(err,obj){
if (!err) {
if (obj.response.numFound > 0) {
deferred.resolve(obj.response.docs);
} else {
deferred.resolve(null);
}
} else {
deferred.reject(err);
}
});
return deferred.promise;
};
To use it you would do something like:
SolrManager.promisedQuery(myquery)
.then(function (data) {
// data is whatever your 'resolved' in promisedQuery
}, function (err) {
// err is whatever you rejected in promisedQuery
});
based on rquinns answer I've changed the code like follows:
SolrManager.prototype.promisedQuery = function(query, callback) {
var solrClient = solr.createClient(this.configuration.cores.page);
var docs = null;
var finished = false;
var deferred = Q.defer();
var request = solrClient.search(query, function(err,obj){
if (!err) {
if (obj.response.numFound > 0) {
deferred.resolve(obj.response.docs);
} else {
deferred.resolve(null);
}
} else {
deferred.reject(err);
}
});
return deferred.promise;
};
...
DemoObject.prototype.toString = function() {
return SolrManager.promisedQuery(this.query).then(function(result){
return result['title'];
}).fail(function(error){
return error;
});
};
DemoObject.prototype.typeOf = function() {
return SolrManager.promisedQuery(this.query).then(function(result){
return result['title'];
}).fail(function(error){
return error;
});
};
I think, this is the right way to use the "promise"-object. But what happens when i do the follwing:
...
var demoObject = new DemoObject();
demoObject.query = "id:1";
console.log(''+demoObject);
...
or if I use "demoObject" by concatenating it to a string
...
var string = "Some string "+demoObject;
...
In case of the string concatenation, I'm currently not sure that the string will contain also the title field from the database. Same for console output.
Will nodejs be so intelligent that it resolves for e.g. the string concatenation "after" the results from the database will be available?
BR
Udo

Multiple Q.all inside function?

I want to send a list of new books to a user. So far the below code works fine. The problem is that I don't want to send a book multiple times, so I want to filter them.
Current code works fine:
function checkActiveBooks(books) {
var queue = _(books).map(function(book) {
var deferred = Q.defer();
// Get all alerts on given keywords
request('http://localhost:5000/books?l=0&q=' + book.name, function(error, response, body) {
if (error) {
deferred.reject(error);
}
var books = JSON.parse(body);
if (!_.isEmpty(books)) {
// Loop through users of current book.
var userBooks = _(book.users).map(function(user) {
// Save object for this user with name and deals.
return {
user: user,
book: book.name,
books: books
}
});
if (_.isEmpty(userBooks)) {
deferred.resolve(null);
} else {
deferred.resolve(userBooks);
}
} else {
deferred.resolve(null);
}
});
return deferred.promise;
});
return Q.all(queue);
}
But now I want to filter already sent books:
function checkActiveBooks(books) {
var queue = _(books).map(function(book) {
var deferred = Q.defer();
// Get all alerts on given keywords
request('http://localhost:5000/books?l=0&q=' + book.name, function(error, response, body) {
if (error) {
deferred.reject(error);
}
var books = JSON.parse(body);
if (!_.isEmpty(books)) {
// Loop through users of current book.
var userBooks = _(book.users).map(function(user) {
var defer = Q.defer();
var userBook = user.userBook.dataValues;
// Check per given UserBook which books are already sent to the user by mail
checkSentBooks(userBook).then(function(sentBooks) {
// Filter books which are already sent.
var leftBooks = _.reject(books, function(obj) {
return sentBooks.indexOf(obj.id) > -1;
});
// Save object for this user with name and deals.
var result = {
user: user,
book: book.name,
books: leftBooks
}
return deferred.resolve(result);
});
return Q.all(userBooks);
} else {
deferred.resolve(null);
}
});
return deferred.promise;
});
return Q.all(queue);
}
But above code doesn't work. It doesn't stop looping. I thought it made sense to use q.all twice, because it contains two loops. But I guess I'm doing it wrong...
First of all you should always promisify at the lowest level. You're complicating things here and have multiple deferreds. Generally you should only have deferreds when converting an API to promises. Promises chain and compose so let's do that :)
var request = Q.nfbind(require("request")); // a promised version.
This can make your code in the top section become:
function checkActiveBooks(books) {
return Q.all(books.map(function(book){
return request('http://.../books?l=0&q=' + book.name)
.get(1) // body
.then(JSON.parse) // parse body as json
.then(function(book){
if(_.isEmpty(book.users)) return null;
return book.users.map(function(user){
return {user: user, book: book.name, books: books };
});
});
});
}
Which is a lot more elegant in my opinion.
Now, if we want to filter them by a predicate we can do:
function checkActiveBooksThatWereNotSent(books) {
return checkActiveBooks(books).then(function(books){
return books.filter(function(book){
return checkSentBooks(book.book);
});
});
}
It's worth mentioning that the Bluebird library has utility methods for all this like Promise#filter and Promise#map that'd make this code shorter.
Note that if checkSentBook is asynchronous you'd need to modify the code slightly:
function checkActiveBooksThatWereNotSent(books) {
return checkActiveBooks(books).then(function(books){
return Q.all(books.map(function(book){ // note the Q.all
return Q.all([book, checkSentBooks(book.book)]);
})).then(function(results){
return results.filter(function(x){ return x[1]; })
.map(function(x){ return x[0]; });
});
});
}
Like I said, with different libraries this would look a lot nicer. Here is how the code would look like in Bluebird which is also two orders of magnitude faster and has good stack traces and detection of unhandled rejections. For fun and glory I threw in ES6 arrows and shorthand properties:
var request = Promise.promisify(require("request"));
var checkActiveBooks = (books) =>
Promise.
map(books, book => request("...&q=" + book.name).get(1)).
map(JSON.parse).
map(book => book.users.length ?
book.users.map(user => {user, books, book: book.name) : null))
var checkActiveBooksThatWereNotSent = (books) =>
checkActiveBooks(books).filter(checkBookSent)
Which I find a lot nicer.
Acting on #Benjamins's suggestion, here is what the code would look like when checkSentBooks returns a promise:
var request = Q.nfbind(require("request")); // a promised version.
function checkActiveBooks(books) {
return Q.all(_(books).map(function(book) {
// a callback with multiple arguments will resolve the promise with
// an array, so we use `spread` here
return request('http://localhost:5000/books?l=0&q=' + book.name).spread(function(response, body) {
var books = JSON.parse(body);
if (_.isEmpty(books)) return null;
return Q.all(_(book.users).map(function(user) {
return checkSentBooks(user.userBook.dataValues).then(function(sentBooks) {
// ^^^^^^ return a promise to the array for `Q.all`
return {
user: user,
book: book.name,
books: _.reject(books, function(obj) {
return sentBooks.indexOf(obj.id) > -1;
})
};
});
}));
});
}));
}

Categories

Resources