The proper ways to stack optioned promises - javascript

What would be the proper or the best way to collect all data from DB with promises, but with using native Node promises.
The goal is only to present what is selected:
const allPromises = [];
const selected = {
sectionA: true,
sectionB: false,
sectionCIds: [ 1, 2, 4 ],
};
if (selected.sectionA) {
allPromises.push(getSectionADataFromDbPromise());
}
if (selected.sectionB) {
allPromises.push(getSectionBDataFromDbPromise());
}
if (selected.sectionCIds.length > 0) {
allPromises.push(selected.sectionCIds
.map(getSectionCDataFromDbPromise)
);
}
Promise.all(allPromises)
.then((allResults) => {
if (selected.sectionA) {
dataA = allResults[0];
}
if (selected.sectionA) {
dataB = allResults[1];
}
if (selected.sectionC) {
dataC = allResults[2]; // <-- this will not work if B is not selected
}
// ... same logic to build report: return Promise.all()...
});
Possible solutions:
Track index for each data selected (eg. index of C will be 1)
Object Map
Add else { allPromises.push(Promise.resolve(null)) } to every if
Is there maybe an easier or one of this will be the proper way?

Don't use push on the arrays conditionally, but always put the same value at the same index. Even if the value is nothing - Promise.all will handle that just fine.
const selected = {
sectionA: true,
sectionB: false,
sectionCIds: [ 1, 2, 4 ],
};
Promise.all([
selected.sectionA ? getSectionADataFromDbPromise() : null,
selected.sectionB ? getSectionBDataFromDbPromise() : null,
Promise.all(selected.sectionCIds.map(getSectionCDataFromDbPromise))
]).then(([dataA, dataB, dataC]) => {
if (selected.sectionA) {
// use dataA
}
if (selected.sectionA) {
// use dataB
}
if (dataC.length) { // same as selected.selectionCIds.length
// use dataC
}
});

What do you think about this ? It's bigger, it's heavier, it's more difficult, but it's all automatized and fully evolutive. Wanna handle a new parameter ? A parameter have data now ? Change the map only.
I create a map that would contains everything we need to use a loop. The state of the data (activated or not), the function to call to get the data and so on.
const mapSelected = {
sectionA: {
state: true,
func: getSectionADataFromDbPromise,
},
sectionB: {
state: false,
func: getSectionBDataFromDbPromise,
},
sectionC: {
state: true,
func: getSectionCDataFromDbPromise,
data: [
1,
2,
4,
],
},
};
Then we create the promise array using the map we has created. Handling the case with data and without data.
const promises = Object.values(mapSelected).reduce((tmp, {
state,
func,
data,
}) => {
if (!state) return tmp;
if (data && data.length) {
return [
...tmp,
...data.map(x => func.call(this, x)),
];
}
return [
...tmp,
func.call(this),
];
});
Then we create arrays from the promise return for each key on the map. You can change how I present the data, I didn't knew what you really wanted there.
Promise.all(promises)
.then((allResults) => {
let i = 0;
const [
dataA,
dataB,
dataC,
] = Object.values(mapSelected).reduce((tmp, {
state,
data,
}, xi) => {
if (!state) return tmp;
if (data && data.length) {
data.forEach(x => (tmp[xi].push(allPromises[i++])));
return tmp;
}
tmp[xi].push(allPromises[i++]);
return tmp;
}, Object.values(mapSelected).map(() => []));
});
#EDIT
I just did a snippet about the code I've made, run it
function a() {
return 1;
}
const mapSelected = {
sectionA: {
state: true,
func: a,
},
sectionB: {
state: false,
func: a,
},
sectionC: {
state: true,
func: a,
data: [
1,
2,
4,
],
},
};
const allPromises = [
0,
1,
2,
3,
4,
];
let i = 0;
const [
dataA,
dataB,
dataC,
] = Object.values(mapSelected).reduce((tmp, {
state,
data,
}, xi) => {
if (!state) return tmp;
if (data && data.length) {
data.forEach(x => (tmp[xi].push(allPromises[i++])));
return tmp;
}
tmp[xi].push(allPromises[i++]);
return tmp;
}, Object.values(mapSelected).map(() => []));
console.log(dataA);
console.log(dataB);
console.log(dataC);

Unfortunately, unlike libraries such as Q, the standard Promise does not expose a variant of all taking an object of promises.
However, we can use the new ES2015 and ES2017 Object utility methods to assist us in keeping the code readable.
const allPromises = {};
const selected = {
sectionA: true,
sectionB: false,
sectionCIds: [1, 2, 4],
};
if (selected.sectionA) {
allPromises.sectionA = getSectionADataFromDbPromise();
}
if (selected.sectionB) {
allPromises.sectionB = getSectionBDataFromDbPromise();
}
if (selected.sectionBIds.length > 0) {
allPromises.sectionC = Promise.all(selected.sectionBIds
.map(getSectionCDataFromDbPromise)
);
}
Now we can write
Promise.all(Object.entries(allPromises).map(([key, promise]) =>
promise.then(value => ({[key]: value}))
))
.then(allResults => {
const results = Object.assign({}, ...allResults);
const data = {
a: results.sectionA,
b: results.sectionB,
c: results.sectionB && results.sectionC
};
// ... same logic to build report: return Promise.all()...
});

Related

How does if else works?

I'm trying to figure out where my problem comes from in my algorithm.
I am trying to give the information about the connection status of a data sender with its data table.
I have translated it like this:
if new data is received ( new_id different from id_from_last_request) then I set the connection status to "connected" otherwise I set it to "disconnected"
<script>
export default {
data() {
return {
search: '',
tag_id: ['bts_d02c2b7d9098aaa2', 'bts_c077ffaa9098aaa2'],
headers: [
{
text: 'Tags',
align: 'start',
sortable: false,
value: 'name',
},
{ text: 'wifi', value: 'wifi' },
],
val_ia: 0,
desserts: [],
id_memory: [],
}
},
mounted() {
this.CreateTable();
setInterval(this.getDatafor, 1000)
},
methods: {
CreateTable() {
for (let i = 0; i < this.tag_id.length; i++) {
this.desserts.push(
{
name: this.tag_id[i],
},
)
}
},
async getDatafor() {
for (let i = 0; i < this.desserts.length; i++) {
this.val_ia = i;
await Promise.all([this.getAllData()]);
}
},
async getAllData() {
const tag_id_name = encodeURIComponent(this.tag_id[this.val_ia]);
const url = this.$api.getRESTApiUri() + `/all/last_id/${tag_id_name}`;
return fetch(url)
.then(res => res.text())
.then((result) => {
console.log(tag_id_name)
console.log(this.id_memory[this.val_ia]);
console.log(data[0].id)
const b = this.Test(this.id_memory[this.val_ia], data[0].id);
console.log(b)
if(b){
this.desserts[this.val_ia].wifi = 'connecté'
console.log('connecté')
}else{
this.desserts[this.val_ia].wifi = 'déconnecté'
console.log('déconnecté')
}
this.id_memory[this.val_ia] = data[0].id
})
.catch((error) => {
console.log(error)
});
},
Test(x, y) {
const a = x !== y
return a
},
}
}
</script>
Only in case I have no new data
const b = false
here is my console:
I should have the disconnected status only it shows me the connected status
There should be a logical explanation to it but I can't see it..
You are using equality without type coersion (x !== y) in your Test method.
Probably this.id_memory[this.val_ia] and data[0].id have different types - one is number, second one is string or otherwise.
The best solution is to convert those values to the same type before comparing like so:
Test(x,y){
return String(x) !== String(y)
}
Some use cases:
'123' === 123 // false
'123' == 123 // true
When creating my table, I forgot to push variables wifi and bluetooth so they did not update themselves.
CreateTable(){
for(let i = 0; i < this.tag_id.length; i++){
this.desserts.push(
{
name: this.tag_id[i],
wifi: 'déconnecté',
bluetooth: 0,
tension: 0,
courant: 0,
temperature: 0,
acceléromètre: 0,
pression_sys: 0,
pression_dias: 0,
frequence_cardiaque: 0,
taux_oxygène: 0,
},
)
}
},

How to return false from traverse function as well as from inner traverse function to stop the traverse and also need unit test for this?

//a user will call the traverse function like this for example stop the traversal when item found
let foundItem: IGXA.TAnyItem | undefined;
traverse([...], item => {
if (item.ID === "test") {
foundItem = item;
return false
}
})
/**
* Calls the callback for every direct and deep child item in the IGXA item list.
*
*
* #param {TItems} items
* #param {(item: TAnyItem) => void} cb
*/
export const traverse = (
items: TItems,
cb: (item: TAnyItem) => void | false,
) => {
for (const item of items) {
cb(item);
if (
item.Type === EItemType.Folder ||
item.Type === EItemType.MultiArticle
) {
traverse(item.SubItems, cb);
}
}
};
//unit test for the above function items is the array we passed to traverse
describe("traverse", () => {
it("calls the callback for every item", () => {
const items: TItems = [
{
ID: "folder",
Type: EItemType.Folder,
SubItems: [
{
ID: "folder_1",
Type: EItemType.Folder,
SubItems: [
{
ID: "article",
Type: EItemType.ArticleGfx,
Caption: {},
},
],
Caption: {},
},
],
Caption: {},
},
{
ID: "multiarticle",
Type: EItemType.MultiArticle,
Caption: {},
MultiArticleId: "",
SubItems: [
{
ID: "article2",
Type: EItemType.ArticleGfx,
Caption: {},
},
],
},
{
ID: "article3",
Type: EItemType.ArticleGfx,
Caption: {},
},
];
const cb = jest.fn();
traverse(items, cb);
expect(cb).toHaveBeenCalledTimes(6);
expect(cb).toHaveBeenCalledWith(items[items.length - 1]);
});
});
Generators are a perfect fit for this -
function* traverse (t) {
switch (t?.constructor) {
case Array:
for (const v of t)
yield *traverse(v)
break
case Object:
yield t
yield *traverse(t?.SubItems)
break
}
}
Q: "How to return false from traverse function as well as from inner traverse function to stop the traverse?"
A: Generators are pauseable and cancellable so you can easily stop iteration -
function findById (graph, id) {
for (const node in traverse(graph))
if (node.ID === id)
return node // <- return stops iteration and stops the generator
}
console.log(findById(TItems, "folder_1"))
{
ID: "folder_1",
Type: EItemType.Folder,
SubItems: [
{
ID: "article",
Type: EItemType.ArticleGfx,
Caption: {},
},
],
Caption: {},
}
Q: "...and also need unit test for this?"
A: Make a simple graph as the test input -
const mygraph = [
{
id: 1,
SubItems: [
{ id: 2 },
{ id: 3 }
]
},
{ id: 4 },
{
id: 5,
SubItems: [
{
id: 6,
SubItems: [ { id: 7 } ]
},
{ id: 8 }
]
}
]
Then write some simple tests -
it("traverse should visit all of the nodes", _ => {
const ids = Array.from(traverse(mygraph), node => node.id)
assert.strictEqual(ids, [1,2,3,4,5,6,7,8])
})
it("findById should find a node", _ => {
const match = findById(mygraph, 6)
assert.strictEqual(match.id, 6)
assert.strictEqual(match.SubItems.length, 1)
})
it("findById should return undefined for unmatched id", _ => {
const match = findById(mygraph, 99)
assert.strictEqual(match, undefined)
})
Q: "thanks for u detailed answer but i need a callback version too? how to achieve it"
A: rewrite traverse and implement your own yield which passes a resume control to the caller -
function traverse (t, yield, resume = () => void 0) {
switch (t?.constructor) {
case Array:
if (empty(t))
return resume()
else
return traverse(head(t), yield, _ => traverse(tail(t), yield, resume))
case Object:
return yield(t, _ => traverse(t?.SubItems, yield) || resume())
}
}
This depends on a few helpers, empty, head and tail. These aren't necessary but keep the code in traverse a bit cleaner and easier to read -
const head = t => t[0]
const tail = t => t.slice(1)
const empty = t => t.length < 1
Here's findById. Traversal will only continue if resume is called -
function findById (graph, id) {
let found = undefined
traverse(graph, (node, resume) =>
node.id === id ? (found = node) : resume()
)
return found
}
console.log(findById(mygraph, 6))
{ id: 6, SubItems: [ { id: 7 } ] }
Another difference is callback-based traverse cannot plug directly into Array.from. To build an array of all visited nodes, we can push to an array in each callback and unconditionally resume -
function getIds (t) {
const r = []
traverse(t, (node, resume) =>
r.push(node.id) && resume()
)
return r
}
console.log(getIds(mygraph))
[1,2,3,4,5,6,7,8]
For this version, you may wish to implement another unit test that verifies traversal is lazy -
it("should stop traversal if resume is not called", _ => {
const input = [ { id: 1 }, { id: 2 } ]
const actual = []
traverse(input, (node, resume) => {
actual.push(node.id)
// do not resume()
})
assert.strictEqual(actual, [1], "should only contain the first item")
})
Expand the snippet below to verify the the program in your own browser -
const head = t => t[0]
const tail = t => t.slice(1)
const empty = t => t.length < 1
function traverse (t, yield, resume = () => void 0) {
switch (t?.constructor) {
case Array:
if (empty(t))
return resume()
else
return traverse(head(t), yield, _ => traverse(tail(t), yield, resume))
case Object:
return yield(t, _ => traverse(t?.SubItems, yield) || resume())
}
}
function getIds (graph) {
const r = []
traverse(graph, (node, resume) =>
r.push(node.id) && resume()
)
return r
}
function findById (graph, id) {
let found = undefined
traverse(graph, (node, resume) =>
node.id === id ? (found = node) : resume()
)
return found
}
const mygraph =
[{id:1,SubItems:[{id:2},{id:3}]},{id:4},{id:5,SubItems:[{id:6,SubItems:[{id:7}]},{id:8}]}]
console.log(JSON.stringify(getIds(mygraph)))
console.log(findById(mygraph, 6))

filter javascript array with a string array with multiple options

I have the following
let foo = ['public', 'private', 'secured', 'unsecured']; // value to search, it can be any combination
['secured', 'unsecured'], ['public', 'secured'] etc...
ARRAY
[
{ id: 1, isPrivate: true, isSecured: true },
{ id: 2, isPrivate: false, isSecured: true },
{ id: 3, isPrivate: true, isSecured: false },
{ ID: 4, isPrivate: false, isSecured: false }
];
[...items].filter(x => filterLabel(x, foo));
filterLabel(x, foo): boolean {
switch (foo[0]) {
case 'private': return x.isPrivate;
case 'public': return !x.isPrivate;
case 'secured': return x.isSecured;
case 'unsecured': return !x.isSecured;
default: return true;
}
This WORKS but it only filters by the first item of the array, i can't figure out how can i filter by using any combination of foo
Example: ['public', 'secured', 'unsecured'];
This would filter the array [...items] by item.isPrivate = false, item.isSecured = true, item.isSecured = false
Example: ['public', 'unsecured'];
This would filter the array [...items] by item.isPrivate = false, item.isSecured = false
Example: ['private', 'unsecured'];
This would filter the array [...items] by item.isPrivate = true, item.isSecured = false
PD: it can be solved by comparing any of the combination but i want to avoid this
const hash = new Set(foo);
const isPrivate = hash.has('private');
const isPublic = hash.has('public');
const isSecured = hash.has('secured');
const isUnsecured = hash.has('unsecured');
if (isPrivate && !isPublic && !isSecured && !isUnsecured) {
return item.isPrivate;
}
if (!isPrivate && isPublic && !isSecured && !isUnsecured) {
return !item.isPrivate;
}
if (!isPrivate && !isPublic && isSecured && !isUnsecured) {
return item.isSecured;
}
// and so on... with all the combinations
You could filter the array ba taking off contradictional labels and take for the rest a function.
const
filterLabel = filter => {
const
p = ['private', 'public'],
s = ['secured', 'unsecured'],
fn = {
private: ({ isPrivate }) => isPrivate,
public: ({ isPrivate }) => !isPrivate,
secured: ({ isSecured }) => isSecured,
unsecured: ({ isSecured }) => !isSecured
};
if (p.every(v => filter.includes(v)) filter = filter.filter(v => !p.includes(v));
if (s.every(v => filter.includes(v)) filter = filter.filter(v => !s.includes(v));
return o => filter.every(k => fn[k](o));
},
foo = ['public', 'private', 'secured', 'unsecured'],
result = items.filter(filterLabel(foo));

track path of recursive function

I'm trying to track path of a deep nested value in json object but having hard time getting the path. Each Item is an array of objects and can have child items. If the object c exists in the json data it is always located in the last item array.
item: [
{
a:5,
item: [
{
item: [
{c:1},
{x:4},
],
...
},
{},
{}
]
},
{},
{}
]
const findPath = (items) => {
let path = []
items.forEach((item,i) => {
if('item' in item){
path = path.concat(findPath(item.item))
}
else if('c' in item) {
path.push(i)
}
})
return path
}
if I have 3 c objects with different item depths, then I would have:
[
[0,0,0], //item[0].item[0].item[0].c
[1,0], //item[1].item[0].c
[4]] , //item[4].c
Any help?
Your main problem here is that you don't track the common case. You store the index only when you found a leaf, but you want all the steps in between. This being recursion, you also have to carry your return values with you, or you end up stepping on them. This works:
objects = [
{},
{
item: [
{},
{},
{
a:5,
item: [
{
item: [
{c:1},
{x:4},
]
},
{},
{}
]
},
{}
]
}
]
const findPath = (items, current_path, matching_paths) => {
items.forEach((item,i) => {
if('item' in item){
current_path.push(i);
current_path = current_path.concat(
findPath(item.item, current_path, matching_paths)
);
}
else if('c' in item) {
current_path.push(i);
matching_paths.push( current_path.slice() );
current_path = [];
}
})
}
var path = [];
var paths = [];
findPath(objects, path, paths);
console.log(paths); //[[1, 2, 0, 0]]
If C is found push a path object to the path array and update that path object for the rest of the paths.
const findPath = (items) => {
let path = []
items.forEach((item,i) => {
if('item' in item){
let item_path = findPath(item.item)
if(item_path.length > 0){
item_path[0].path.push(i)
path.push(item_path[0])
}
}
else if('c' in item){
path.push({path:[i], c:item.c})
}
})
return path
}
The function must be recursive, which means it should call itself with different parameters and not loop forever.
Below is what you are looking for. I made it in TypeScript to make sure I typed it correctly, but just take off all type definitions and it becomes JavaScript:
const trackPath: number[][] = [];
function findPath(topItem: any, path: number[], position: number): void
{
const currentPath = path.slice();
currentPath.push(position);
const newTopItem = topItem['item'];
if (Array.isArray(newTopItem)) {
// here is the recursion for each subitem
newTopItem.forEach((item, i) => findPath(item, currentPath, i));
}
if ('c' in topItem) {
trackPath.push(currentPath);
}
}
// this is the main method to call
function actuallyGetThePath(myTopItem: any): number[][] {
findPath(myTopItem, [], 0);
return trackPath;
}
Good luck!

RxJs: Paginate through API recursively and find value from list

I am using rxjs v6.4.0. I am trying to paginate through an API searching for a very specific channel where name equals "development". I am using expand to recursively call the API and get new pages. The end result gives me a concatenated list of channels. Then I filter out all channels where name not equal to "development". However I am getting an error: TypeError: You provided 'undefined' where a stream was expected. You can provide an Observable, Promise, Array, or Iterable.
const Rx = require('rxjs')
const Rx2 = require('rxjs/operators')
const getChannel = (cursor) => {
return this.service.getData(`${url}?cursor=${cursor || ''}`)
.pipe(Rx2.map(resp => JSON.parse(resp.body)))
.pipe(Rx2.expand(body => { // recurse the api until no more cursors
return body.response_metadata &&
body.response_metadata.next_cursor ?
getChannel(body.response_metadata.next_cursor) : Rx.EMPTY
}))
.pipe(Rx2.pluck('channels'))
.pipe(Rx2.mergeAll()) // flattens array
.pipe(Rx2.filter(c => {
console.log('finding', c.name === 'development')
return c.name === 'development'
}))
}
The find callback should return a boolean, not an Observable. E.g.
find(c => c.name === 'development')
UPDATE
Heres a modified example of yours. I've removed generators as they are more complicated then our case needs.
const { of, EMPTY, throwError } = rxjs;
const { filter, tap, expand, pluck, mergeAll } = rxjs.operators;
const DATA =
[ {channels: [{id: 123, name: 'test'}, {id:4, name: 'hello'}], cursor: 1}
, {channels:[{id: 1, name: 'world'}, {id: 2, name: 'knows'}], cursor: 2}
, {channels:[{id: 3, name: 'react'}, {id: 5, name: 'devcap'}], cursor: false}
];
function getChannel(){
return getBlock()
.pipe(
expand(x => x.cursor ? getBlock(x.cursor) : EMPTY),
pluck('channels'),
mergeAll(),
filter(c => c.name === 'devcap')
)
}
getChannel().subscribe({
next: console.log,
error: console.error
});
function getBlock(index = 0) {
if (index >= DATA.length){
throwError('Out of bounds');
}
return of(DATA[index]);
}
UPDATE 2
Your solution didn't work due to recursion being done through solely getChannel(). When you do the expand -- you run another cycle through getChannel(). Meaning that you run pluck-mergeAll-filter chain twice on each recursively fetched value! Plucking and flattering it twice gives you undefined -- therefore the error.
In your playground -- try separating out this code
let getValue = ()=>{
const next = gen.next();
if (!next || !next.value) { return EMPTY; }
return next.value;
}
and use it in the expand, like this:
let getChannel = () => {
return getValue()
.pipe(
expand(body => {
return body.cursor ? getValue() : EMPTY
}),
pluck('channels'),
mergeAll(),
filter(c => c.name === 'devcap'),
)
}
Let me know if this is the functionality you are looking for https://codepen.io/jeremytru91/pen/wOQxbZ?editors=1111
const {
of,
EMPTY
} = rxjs;
const {
filter,
tap,
expand,
take,
pluck,
concatAll,
flatMap,
first
} = rxjs.operators;
function* apiResponses() {
yield of({channels: [{id: 123, name: 'test'}, {id:4, name: 'hello'}], cursor: 1});
yield of({channels:[{id: 3, name: 'react'}, {id: 5, name: 'devcap'}], cursor:3});
yield of({channels:[{id: 1, name: 'world'}, {id: 2, name: 'knows'}], cursor: 2});
yield of({channels:[{id: 4, name: 'react'}, {id: 6, name: 'devcap'}], cursor:4});
}
let gen = apiResponses();
function getChannel() {
return gen.next().value // simulate api call
.pipe(
expand(body => {
return body.cursor ? gen.next().value : EMPTY
}),
pluck('channels'),
flatMap(channels => {
const filtered = channels.filter(channel => channel.name === 'devcap')
if(filtered.length) {
return filtered;
}
return EMPTY;
}),
first()
);
}
getChannel().subscribe(data => {
console.log('DATA!! ', data)
}, e => {
console.log('ERROR', e)
throw e
})

Categories

Resources