I could use a bit of help. I'm trying to an experiment where I start my ajax request on mouseenters of my button, but only consume the result of that request if the user clicks (or if the request hasn't finished yet when the user clicks, consume the result as soon as it does). If the user leaves the button without clicking, the request should be cancelled.
Where I'm stuck is how to merge the click stream with the request stream. I cannot use withLatestFrom, because then if the request finishes after the click, it will not be consumed. I also cannot use combineLatest, because then if any click has occurred in the past, the the request will be consumed, even if I'm currently just mousing over.
Would love some guidance. It's been a fun problem to think about but I'm stuck
const fetchContent = (url) => {
const timeDelay$ = Rx.Observable.timer(1000); // simulating a slow request
const request$ = Rx.Observable.create(observer =>
fetch(url, { mode: 'no-cors' })
.then(json => {
observer.onNext('res')
observer.onCompleted()
})
.catch(e => observer.onError())
)
return timeDelay$.concat(request$)
}
const hover$ = Rx.Observable.fromEvent(myButton, 'mouseenter')
const leave$ = Rx.Observable.fromEvent(myButton, 'mouseleave')
const click$ = Rx.Observable.fromEvent(myButton, 'click')
const hoverRequest$ = hover$
.flatMap(e =>
fetchContent(e.target.getAttribute('href'))
.takeUntil(leave$.takeUntil(click$))
)
const displayData$ = click$
.combineLatest(hoverRequest$)
displayData$.subscribe(x => console.log(x))
You aren't terribly far off actually. You are just missing the inclusion of zip really. Since what you really need for propagation is for both a mouse click and the request to complete. By zipping the request and the mouse click event you can make sure that neither emits without the other.
const hover$ = Rx.Observable.fromEvent(myButton, 'mouseenter');
const leave$ = Rx.Observable.fromEvent(myButton, 'mouseleave');
const click$ = Rx.Observable.fromEvent(myButton, 'click');
//Make sure only the latest hover is emitting requests
hover$.flatMapLatest(() => {
//The content request
const pending$ = fetchContent();
//Only cancel on leave if no click has been made
const canceler$ = leave$.takeUntil(click$);
//Combine the request result and click event so they wait for each other
return Rx.Observable.zip(pending$, click$, (res, _) => res)
//Only need the first emission
.take(1)
//Cancel early if the user leaves the button
.takeUntil(canceler$);
});
Maybe you could conceptualize this as three events (hover, leave, click as you call them) triggering three actions (emit request, cancel request, pass request result) modifying a state (request? pass request?).
Now this is done in a rush on a sunday evening, but with a little bit of luck , something like this could work :
function label(str) {return function(x){var obj = {}; obj[str] = x; return obj;}}
function getLabel(obj) {return Object.keys(obj)[0];}
const hover$ = Rx.Observable.fromEvent(myButton, 'mouseenter').map(label('hover'));
const leave$ = Rx.Observable.fromEvent(myButton, 'mouseleave').map(label('leave'));
const click$ = Rx.Observable.fromEvent(myButton, 'click').map(label('click'));
var initialState = {request : undefined, response : undefined, passResponse : false};
var displayData$ = Rx.Observable.merge(hover$, leave$, click$)
.scan(function (state, intent){
switch (getLabel(intent)) {
case 'hover' :
if (!state.request) {
state.request = someRequest;
state.response$ = Rx.Observable.fromPromise(executeRequest(someRequest));
}
break;
case 'leave' :
if (state.request && !state.passResponse) cancelRequest(someRequest);
state.passResponse = false;
break;
case 'click' :
if (!state.request) {
state.response$ = Rx.Observable.fromPromise(executeRequest(someRequest));
}
state.passResponse = true;
}
}, initial_state)
.filter(function (state){return state.passResponse;})
.pluck('response$')
.concatAll()
Related
So I'm working on a project where I'm making a call to a database to retrieve the data stored there. This data comes as an array. here is the code:
const allLogins = await Login.find().sort("name");
const token = req.header("x-auth-token");
const user = jwt.verify(token, config.get("jwtPrivateKey"));
const logins = allLogins
.filter((login) => login.userId === user._id)
.map((login) => {
login.password = decrypt(login.password);
});
If I call a console.log after the decrypt has been run I see that it has been completed correctly. The issue I have is if I console.log(logins) it says it is an array of two items that are both undefined. If instead I run it like this...
const allLogins = await Login.find().sort("name");
const token = req.header("x-auth-token");
const user = jwt.verify(token, config.get("jwtPrivateKey"));
let logins = allLogins.filter((login) => login.userId === user._id);
logins.map((login) => {
login.password = decrypt(login.password);
});
Then it works as it should. I'm not sure why the first set of code doesn't work and why the second set does work.
Any help would be appreciated!
Basic :
array. filter - accept a callback and call back return boolean (that match our criteria)
array.map - accept a callback and call back return transformed object
In the second working example:
logins.map((login) => {
// note: logins is iterated but not assigned to logins back
// so accessing login is working
login.password = decrypt(login.password); // map should return data
+ return login; // if we update all code will work
});
Now coming to first example:
const logins = allLogins
.filter((login) => login.userId === user._id)
.map((login) => {
login.password = decrypt(login.password);
+ return login; // this will fix the issue
});
I have a section with files. Each file can be moved one position higher or lower with an arrow. Currently a request is sent to the database every time that the user moves a file, and new order updated. So if the user wants to move the bottom file to the top, clicks the 'up' arrow 10 times, 10 requests will be sent.
How can I send fewer requests than that?
My first idea was to create request with order numbers, wait 1 second, create second request and if they are the same (the user hasn't continued to change the order), then send it, if not, wait another second and compare again. But this doesn't seem to be a good thing to do. Are there any other ways?
$ctrl.saveFileOrder = function() {
var firstRequest = [];
for(var i = 0; 1 < $ctrl.fileArray.length; i++) {
firstRequest.push({$ctrl.fileArray[i].id, $ctrl.fileArray[i].orderNumber});
}
var secondRequest = [];
while(firstRequest != secondRequest) {
//put thread to sleep for 1 second here
for(var i = 0; 1 < $ctrl.fileArray.length; i++) {
secondRequest.push({$ctrl.fileArray.id[i], $ctrl.fileArray.orderNumber[i]});
}
}
//send the final request
}
You could disable the button until a response has been received from the server so that the user cannot click again before the DB has been updated.
const buttons = document.querySelectorAll('.js-move');
/**
* Simulation of a request with a 2 second duration
*/
const sendRequest = value => new Promise(resolve => {
console.log(`Updating to DB: ${value}`);
buttons.forEach(button => button.disabled = true);
setTimeout(() => {
console.log('Update done');
buttons.forEach(button => button.disabled = false);
resolve();
}, 2000);
});
buttons.forEach(button => {
button.addEventListener('click', event => {
const { value } = event.target;
sendRequest(value);
});
});
<button class="js-move" value="-1">Move up</button>
<button class="js-move" value="+1">Move down</button>
Alternatively you are looking for a throttle function which, like the name suggest, bottlenecks the amount of calls by a specified amount set by you. So only once every second could you make a new request.
const throttle = (callback, wait, immediate = false) => {
let timeout = null;
let initialCall = true;
return (...args) => {
const callNow = immediate && initialCall
const next = () => {
callback(...args);
timeout = null;
}
if (callNow) {
initialCall = false;
next();
}
if (!timeout) {
timeout = setTimeout(next, wait);
}
}
}
const buttons = document.querySelectorAll('.js-move');
/**
* Mock function where you send a request.
*/
const sendRequest = value => {
console.log(`Updating to DB: ${value}`);
}
/**
* Create throttled version of your request.
* The number indicates the interval in milliseconds between
* calling the sendRequest function.
*/
const throttledRequest = throttle(sendRequest, 1000, true);
buttons.forEach(button => {
button.addEventListener('click', event => {
const { value } = event.target;
throttledRequest(value);
});
});
<button class="js-move" value="-1">Move up</button>
<button class="js-move" value="+1">Move down</button>
I am trying to do multiple asynchronous actions: Axios requests inside of a for loop. I want to do something after everything is resolved but there is so much going on I don't know how to do it.
I thought of making my sourcer function async and awaiting it on each iteration (and wrapping the for loop in an async function), but one problem is that sourcer doesn't actually return anything. I don't know how to return from sourcer from inside an Axios "finally" clause. Another problem is that I don't want to await each sourcer call because it would be a hit on performance.
Promise.all sounds like the right direction to take but I don't know how to implement it with this for loop.
Here is the relevant part of my code (ts is a large array of objects):
.then(ts => {
// Create an association object that determines each media item's source
const sourcer = media => { // Input is either [image filename, image url] or [image filename, image url, video filename, video url]
// Test to see if the original URL works
let validURL = true
axios.get(media[1])
.then(resp => {
if (resp.status.toString()[0] !== '2') validURL = false
})
.catch(resp => {
if (resp.status.toString()[0] !== '2') validURL = false
})
.finally(() => {
let newSources = JSON.parse(JSON.stringify(this.state.sources))
let newModals = JSON.parse(JSON.stringify(this.state.modals))
if (validURL) newSources[media[0]] = media[1]
// If the original URL does not work, pull media item from server
else newSources[media[0]] = `http://serveripaddress/get_media?filename=${media[0]}`
newModals[media[0]] = false
this.setState({ sources: newSources, modals: newModals })
})
if (media.length > 2) { // If the media item is a video, do the same checks
let validVURL = true
axios.get(media[3])
.then(resp => {
if (resp.status.toString()[0] !== '2') validVURL = false
})
.catch(resp => {
if (resp.status.toString()[0] !== '2') validVURL = false
})
.finally(() => {
let newSources2 = JSON.parse(JSON.stringify(this.state.sources))
let newThumbnails = JSON.parse(JSON.stringify(this.state.thumbnails))
if (validVURL) newSources2[media[2]] = media[3]
else newSources2[media[2]] = `http://serveripaddress/get_media?filename=${media[2]}`
newThumbnails[media[0]] = media[2] // Add an association for the video and its thumbnail
this.setState({ sources: newSources2, thumbnails: newThumbnails })
})
}
}
for (let t of ts) {
if (t.media) for (let m of t.media) sourcer(m)
if (t.preview_media) sourcer(t.preview_media)
if (t.video) sourcer(t.video)
}
})
I want to do something after ts has been iterated through and all sourcer calls are completed.
I'm not fishing for someone to write my code for me but a nudge in the right direction would be greatly appreciated.
axios.get will return a Promise, so simply build up your array of Promises and use Promise.all
So, in your case, instead of executing the http call and waiting on the response, just add it to your array.
Something like this will work. I removed your code that was handling the response of each individual get request. You can merge that code (or just copy/paste) into where I put the placeholder below:
.then(ts => {
// Create an association object that determines each media item's source
const sourcer = media => { // Input is either [image filename, image url] or [image filename, image url, video filename, video url]
// Test to see if the original URL works
let validURL = true;
const promises = [];
promises.push(axios.get(media[1]));
if (media.length > 2) { // If the media item is a video, do the same checks
let validVURL = true;
promises.push(axios.get(media[3]));
}
}
for (let t of ts) {
if (t.media)
for (let m of t.media) sourcer(m)
if (t.preview_media) sourcer(t.preview_media)
if (t.video) sourcer(t.video)
}
// Execute the Promises
Promise.all(promises).then( results => {
const media1 = results[0];
const media3 = results[1];
// TODO: Run your code for media1/media3 results
})
})
I am writing the acceptance tests for my application's login feature. At some point, I want to double-check the cookie's expiry time.
Upon clicking on the "Login" button, a graphql query is sent to my server which responds with a Jwt. Upon reception of the jwt, the application sets the cookie with
document.cookie = ...
In my Cypress test, I check the token in the following way:
Then("sa session s'ouvre pour {SessionDurationType}", expectedDuration => {
cy.get('#graphql').then(() => {
cy.wait(1000)
cy.getCookie('token').then(cookie => {
const tokenDuration = getTokenDuration(cookie.value)
expect(tokenDuration.asSeconds()).to.equal(expectedDuration.asSeconds())
})
})
})
With cy.get('#graphql'), I am waiting for the graphql query to return a response. The alias is defined like this:
cy.stub(win, 'fetch', fetch).as('graphql')
Upon reception, the application sets the cookie.
My problem is that I am not fond of the following call:
cy.wait(1000)
Without that call, I always get an undefined cookie.
Is there a way to get that cookie within some time that might be much less than 1000 ms? I tried many things without success...
You must write a recursive promise function, try the following
function checkCookie() {
// cy.getCookie returns a thenebale
return cy.getCookie('token').then(cookie => {
const tokenDuration = getTokenDuration(cookie.value);
// it checks the seconds right now, without unnecessary waitings
if(tokenDuration.asSeconds() !== expectedDuration.asSeconds()) {
// waits for a fixed milliseconds amount
cy.wait(100);
// returns the same function recursively, the next `.then()` will be the checkCookie function itself
return checkCookie();
}
// only when the condition passes returns a resolving promise
return Promise.resolve(tokenDuration.asSeconds());
})
}
Then("sa session s'ouvre pour {SessionDurationType}", expectedDuration => {
cy.get('#graphql').then(() => {
checkCookie()
.then(seconds => {
expect(seconds).to.equal(expectedDuration.asSeconds())
})
})
})
Note that the function must be improved because
I didn't parametrize the expectedDuration etc. (it's out of the scope of showing you how to do that)
it waits forever without a loop counter check
But it works (I checked in another context before replying to you) and if you have some more troubles please share a "working" GitHub repo so I can clone and check it with your own solution.
Let me know if it isn't enough clear 😉
UPDATE
We (me and Tommaso) have written a plugin to help you with this kind of checks, its name is cypress-wait-until.
Please thank the Open Source Saturday community for that, we developed it during one of them Saturdays 😊
I dont like the timeout in this i have to say for dom changes. I have come up with this solution based on #NoriSte Answer together with DomMutation Observers.
getFileUploadItem().get(".upload-item--state i")
.should("have.class", "ngx-fileupload-icon--start")
.then(item => {
const iconEl = item.get(0);
const states: string[] = [];
return new Promise((resolve, reject) => {
const observer = new MutationObserver((mutations: MutationRecord[]) => {
const mutationEl = mutations[0].target as HTMLElement;
const className = mutationEl.getAttribute("class");
states.push(className);
if (className === "ngx-fileupload-icon--uploaded") {
resolve(states);
}
});
observer.observe(iconEl, {
subtree: true,
attributes: true,
attributeFilter: ["class"]
});
});
})
.then((value) => expect(value).to.deep.equal(
["ngx-fileupload-icon--progress", "ngx-fileupload-icon--uploaded"])
);
Based on #NoriSte's answer, I came up with the following working code:
function awaitNonNullToken(elapsedTimeInMs = 0) {
let timeDeltaInMs = 10
if (elapsedTimeInMs > Cypress.env('timeoutInMs')) {
return Promise.reject(new Error('Awaiting token timeout'))
}
return getTokenCookie().then(cookie => {
if (cookie === null) {
cy.wait(timeDeltaInMs)
elapsedTimeInMs += timeDeltaInMs
return awaitNonNullToken(elapsedTimeInMs)
}
return Promise.resolve(cookie.value)
})
}
I transformed that into an ES6 class that I find a bit more elegant:
class TokenHandler {
constructor () {
this.TIME_DELTA_IN_MS = Cypress.env('timeDeltaInMs')
this.TIMEOUT_IN_MS = Cypress.env('timeoutInMs')
this.elapsedTimeInMs = 0
}
getToken () {
if (this.elapsedTimeInMs > this.TIMEOUT_IN_MS) {
return Promise.reject(new Error('Awaiting token timeout'))
}
return getTokenCookie().then(cookie => {
if (cookie === null) {
cy.wait(this.TIME_DELTA_IN_MS)
this.elapsedTimeInMs += this.TIME_DELTA_IN_MS
return this.getToken()
}
return Promise.resolve(cookie.value)
})
}
}
and reworked my step like this:
cy.get('#graphql').then(() => {
const handler = new TokenHandler
handler.getToken().then(token => {
const tokenDuration = getTokenDuration(token)
expect(tokenDuration.asSeconds()).to.equal(expectedDuration.asSeconds())
})
})
This is working perfectly, thanks.
I would like to flush a buffered observable based on the content of the buffer, but how to accomplish this? A simplified example of what I want to do:
observable.buffer(() => {
// Filter based on the buffer content.
// Assuming an observable here because buffer()
// needs to return an observable.
return buffer.filter(...);
})
Here is more specifically what I am trying to do with key events (bin here):
const handledKeySequences = ['1|2']
// Mock for document keydown event
keyCodes = Rx.Observable.from([1,2,3,4])
keyCodes
.buffer(() => {
/*
The following doesn't work because it needs to be an
observable, but how to observe what is in the buffer?
Also would like to not duplicate the join and includes
if possible
return function (buffer) {
return handledKeySequences.includes(buffer.join('|'));
};
*/
// Returning an empty subject to flush the buffer
// every time to prevent an error, but this is not
// what I want.
return new Rx.Subject();
})
.map((buffer) => {
return buffer.join('|')
})
.filter((sequenceId) => {
return handledKeySequences.includes(sequenceId);
})
.subscribe((sequenceId) => {
// Expecting to be called once with 1|2
console.log("HANDLING", sequenceId)
})
I feel like my approach is wrong, but I can't figure out what the right approach would be. I've tried using scan, but that scans all the events in the observable, which is not what I want.
Thanks for any help!
This should be doable with bufferWithCount:
const handledKeySequences = ['1|2']
// Mock for document keydown event
keyCodes = Rx.Observable.from([0,1,2,3,4]);
const buffer$ = keyCodes
.bufferWithCount(2, 1) // first param = longest possible sequence, second param = param1 - 1
.do(console.log)
.map((buffer) => {
return buffer.join('|')
})
.filter((sequenceId) => {
return handledKeySequences.includes(sequenceId);
});
buffer$.subscribe((sequenceId) => {
console.log("HANDLING", sequenceId)
});
See live here.
Also have a look at this question.
It seems that this functionality is not currently available in Rxjs, so as suggested by #olsn I wrote a custom operator that works by passing a function to tell when to flush the buffer:
(function() {
// Buffer with function support.
function bufferWithContent(bufferFn) {
let buffer = [];
return this.flatMap(item => {
buffer.push(item);
if (bufferFn(buffer)) {
// Flush buffer and return mapped.
let result = Rx.Observable.just(buffer);
buffer = [];
} else {
// Return empty and retain buffer.
let result = Rx.Observable.empty();
}
return result;
});
}
Rx.Observable.prototype.bufferWithContent = bufferWithContent;
})();
I also opened an issue here that proposes adding this functionality.