Cycle.js HTTP sending multiple requests after adding loading indicator - cyclejs

I have been attempting to create some cycle.js examples as nested dialogues, and switching between them using a select box.
One of the dialogues is a clone of the official Github HTTP Search example.
The other dialogue a more basic one which does not have HTTP, only DOM.
I feel like I wrapped my head around switching between the 2, but I'm fairly new to Rx so that may be done incorrectly or naively.
It all seemed to work well until I added a loading indicator to the search page.
To do that, I turned this:
const vTree$ = responses.HTTP
.filter(res$ => res$.request.indexOf(GITHUB_SEARCH_API) === 0)
.flatMapLatest(x => x)
.map(res => res.body.items)
.startWith([])
.map(results =>
h('div.wrapper', {}, [
h('label.label', {}, 'Search:'),
h('input.field', {type: 'text'}),
h('hr'),
h('section.search-results', {}, results.map(resultView)),
])
)
Into this:
const searchResponse$ = responses.HTTP
.filter(res$ => res$.request.indexOf(GITHUB_SEARCH_API) === 0)
.flatMapLatest(x => x)
.map(res => res.body.items)
.startWith([])
const loading$ = searchRequest$.map(true).merge(searchResponse$.map(false))
// Convert the stream of HTTP responses to virtual DOM elements.
const vtree$ = loading$.withLatestFrom(searchResponse$, (loading, results) =>
h('div.wrapper', {}, [
h('label.label', {}, 'Search:'),
h('input.field', {type: 'text'}),
h('span', {}, loading ? 'Loading...' : 'Done'),
h('hr'),
h('section.search-results', {}, results.map(resultView)),
])
)
I now have 2 issues
The 'checkbox value set to' and 'route changed' messages are logged
twice for every change of the checkbox.
The HTTP request log only
fires once, but if you watch the network activity in Dev Tools
you'll see two GET requests simultaneously.
Thanks for any help!
EDIT: Solved my own problem. See answer below.

I ended up solving this problem by rebuilding my entire application from scratch until I found the breaking point.
What I learned is that you need to add .share() to any observable stream which will be subscribed/mapped/etc by more than one downstream observable.
const searchRequest$ = DOM.select('.field').events('input')
.debounce(500)
.map(ev => ev.target.value.trim())
.filter(query => query.length > 0)
.map(q => GITHUB_SEARCH_API + encodeURI(q))
.share() //needed because multiple observables will subscribe
// Get search results from HTTP response.
const searchResponse$ = HTTP
.filter(res$ => res$ && res$.request.url.indexOf(GITHUB_SEARCH_API) === 0)
.flatMapLatest(x => x) //Needed because HTTP gives an Observable when you map it
.map(res => res.body.items)
.startWith([])
.share() //needed because multiple observables will subscribe
//loading indication. true if request is newer than response
const loading$ = searchRequest$.map(true).merge(searchResponse$.map(false))
.startWith(false)
.share()
//Combined state observable which triggers view updates
const state$ = Rx.Observable.combineLatest(searchResponse$, loading$,
(res, loading) => {
return {results: res, loading: loading}
})
//Generate HTML from the current state
const vtree$ = state$
.map(({results, loading}) =>
.........

Related

Duplicated requests to same url with fetch using vue and webpack

I have weird results displayed in the web console:
fetch() is sending duplicated requests to the same url.
I thought it could be something related to fetch(), but also noticed that on reload of the app (quasar, based on webpack) also the requests to the http://localhost:8080/sockjs-node/info are duplicated.
By contrast, I noticed that requests handled by jQuery are NOT duplicated and works fine.
I cannot say if it is an error due to webpack configuration, fetch or they way I am using it i Vue components.
E.g. This article points out possible causes https://insights.daffodilsw.com/blog/how-to-avoid-duplicate-api-requests but in my case it is not due to user interaction : requests are triggered at time of relaunching the app (webpack), and particularly the stack trace shows that the requests are fired at time of creating the components, just multiple times.
Example of how I am using fetch():
// component -
methods : {
search(input) {
return new Promise(resolve => { // a new promise is request multiple times, in spite in created() it is called just once
var _base = 'myEndpoint/api'
const url = `${_base}fuzzyautocomplete?q=${encodeURI(input)}`
if (input.length < 3) {
return resolve([])
}
fetch(url) // so promises are actually different, and duplicated requests are fired by fetch
.then(response => response.json())
.then(data => {
console.log(data)
// resolve(data.query.search)
resolve(data)
})
})
},
....
// and it should be called just once at time of creation
created() {
this.search('init first query !!');
}
Could you advise?

Assert element exists after all XHR requests finished

I'm visiting a page which is fetching data Asynchronously (multiple XHR requests), and then asserting if a certain DOM element is visible/exists in the page.
So far I was only able to get the page and the data fetched with using cy.wait() either with an arbitrary time, or by aliasing the actual request, and using the promise-like syntax to make sure my cy.get() is done after the XHR response has completed.
Here is what doesn't work:
before(() => {
cy.login();
cy.server();
cy.route('/v0/real-properties/*').as('getRealPropertyDetails');
cy.visit('/real-properties/1/real-property-units-table');
});
beforeEach(() => {
Cypress.Cookies.preserveOnce('platform_session');
});
after(() => {
cy.clearCookies();
});
context('when viewport is below 1367', () => {
it('should be closed by default', () => {
cy.wait('#getRealPropertyDetails'); // the documentation says this is the way to go
sSizes.forEach((size) => {
cy.viewport(size[0], size[1]);
cy.get('.v-navigation-drawer--open.real-property-details-sidebar').should('not.exist');
});
});
Adding cy.wait(1000); in the before() or beforeEach() hooks also works, but this is not really an acceptable solution.
What works, but not sure if this is the way to do this (I would have to add this for every page, would be quite annoying) :
it('should be closed by default', () => {
cy.wait('#getRealPropertyDetails').then(() => {
sSizes.forEach((size) => {
cy.viewport(size[0], size[1]);
cy.get('.real-property-details-sidebar').should('not.be.visible');
});
});
});
I see that you have browser reloads there (beforeEach), which could potentially wipe out the route spy, but not sure why cy.wait().then would work. I would try switching from before to beforeEach though, creating things once is always trickier than letting them be created before each test

ionic-native angular example code to vuejs

With the Ionic Native there is the possibility to use iBeacons via a native-plugin: https://ionicframework.com/docs/native/ibeacon
The example code is written for people that use Ionic with AngularJS, but I'm using VueJS and I cannot figure out how to get this to work:
The Angular version of the Example code:
import { IBeacon } from '#ionic-native/ibeacon/ngx';
constructor(private ibeacon: IBeacon) { }
...
// Request permission to use location on iOS
this.ibeacon.requestAlwaysAuthorization();
// create a new delegate and register it with the native layer
let delegate = this.ibeacon.Delegate();
// Subscribe to some of the delegate's event handlers
delegate.didRangeBeaconsInRegion()
.subscribe(
data => console.log('didRangeBeaconsInRegion: ', data),
error => console.error()
);
delegate.didStartMonitoringForRegion()
.subscribe(
data => console.log('didStartMonitoringForRegion: ', data),
error => console.error()
);
delegate.didEnterRegion()
.subscribe(
data => {
console.log('didEnterRegion: ', data);
}
);
let beaconRegion = this.ibeacon.BeaconRegion('deskBeacon','F7826DA6-ASDF-ASDF-8024-BC5B71E0893E');
this.ibeacon.startMonitoringForRegion(beaconRegion)
.then(
() => console.log('Native layer received the request to monitoring'),
error => console.error('Native layer failed to begin monitoring: ', error)
);
But.. what I expected to work was the following in VueJS:
On top of my component importing it: import { IBeacon } from '#ionic-native/ibeacon/ngx';
And use it like this:
foobar() {
let _ibeacon = IBeacon.Delegate()
alert('Hi iBeacon');
_ibeacon.didStartMonitoringForRegion()
.subscribe(
data => console.log('didStartMonitoringForRegion: ', data),
error => console.error()
);
}
But even the alert isn't shown.
What IS the correct way to use the iBeacon plugin with Vue and ionic?
Quick of of this repo worked. Just had to replace two files.

VUE: Issues with fetching data when I use Pagination

I have a a set of data which I can access for the first page. But as soon as I use the pagination and trying to press next page, then I can't access the data. I get the 401 issue.
created(){
this.fetchNotater();
},
methods: {
fetchNotater(page_url){
let vm = this;
// api/borgernotater works perfectly. But when I use the page_url then it won't accept my api_token
page_url = page_url || 'api/borgernotater';
fetch(`${page_url}?api_token=${window.Laravel.apiToken}`)
.then(res => res.json())
.then(res => {
this.notater = res.data;
vm.makePagination(res);
})
.catch(err => console.log(err))
},
makePagination(res){
let pagination = {
current_page: res.current_page,
last_page: res.last_page,
next_page_url: res.next_page_url,
prev_page_url: res.prev_page_url
};
this.pagination = pagination;
}
You have a typo in your API request URL construction: parameters need to be separated with &. You can see that when you are requesting page 2, you have two questions marks in your URL and that’s an invalid query string, and the API request URL will not be parsed correctly. It should be: ?page=2&api_key=....
With that in mind, it might be helpful to use a third party library, like query-string on NPM, to perform parsing and adding payload to your query string.

Jest unresolved promise do not fail

Jest docs says:
Unresolved Promises
If a promise doesn't resolve at all, this error might be thrown:
(and so on)
In my case this not happen.
I have this test:
test('detect infinite loop', () => {
expect.assertions(1);
const vastPromise = VastUtils.parseFromUrl(infiniteLoopUrl);
const expectedError =
new VastError(VastErrorCodes.WRAPPER_LIMIT_REACHED);
return expect(vastPromise).rejects.toEqual(expectedError);
});
VastUtils simply fetch an XML located at infiniteLoopUrl, parse it, and if this xml point to another xml, VastUtils follow the link, parse the new xml, merge them and repeat the process.
Now, infiniteLoopUrl point to an XML that refers itself, so it is an infinite loop.
"correctly", the code follow xml link infinitely, and never resolve or reject the promise.
I expect above test fail after a certain timeout, but it didn't.
Someone can help me?
Thanks
EDIT:
I'm trying to reproduce an infinite Promise loop with a smaller example, and this is what i've noticed:
this test correctly FAIL after 5s:
test('Promise2', () => {
const genPromise = (): Promise<void> => {
return new Promise((res) => {
setTimeout(() => {
res();
}, 200);
})
.then(() => {
return genPromise();
});
};
const vastPromise = genPromise();
const expectedError =
new VastError(VastErrorCodes.WRAPPER_LIMIT_REACHED);
return expect(vastPromise).rejects.toEqual(expectedError);
});
This test DO NOT FAIL after 5s (jest remain in an infinite loop)
test('Promise', () => {
const genPromise = (prom: Promise<void>): Promise<void> => {
return prom
.then(() => {
return genPromise(Promise.resolve());
});
};
const vastPromise = genPromise(Promise.resolve());
const expectedError =
new VastError(VastErrorCodes.WRAPPER_LIMIT_REACHED);
return expect(vastPromise).rejects.toEqual(expectedError);
});
Apparently these are similar, but I don't understand the difference that cause the jest infinite loop...
Ok, I've understand the problem.
The cause is the mono thread nature of js.
In the two examples of the edit section, te first one have a timeout so there is a moment whent jest take the control and could check the timeout.
In the second one no, so jest never check the timeout.
In my real case, the problem was the fake server: it was created as:
server = sinon.fakeServer.create({
respondImmediately: true
});
respondImmediately make sinon respond syncroniously, so jest never have the control.
Creating it as:
server = sinon.fakeServer.create({
autoRespond: true
});
sinon respond after 10ms and jest can check the time passing