I'm trying to abort any/all previous Axios requests, using AbortController():
https://axios-http.com/docs/cancellation
FAILS: In my testing, previous queries do not get aborted.
The search experience still works as expected, but every request gets fully digested when user slams away on filters. Instead I want all previous requests to just be aborted.
I want to avoid building logic that uses storing/tracking requests, tokens and/or promises. I'm familiar with this stuff and could build it, but just want to avoid all that.
Is the Axios' AbortController intended for this purpose?
UPDATE (WORKS): Thx to #Oluwafemi, my setup is working.
Two things had to be changed:
Set a new instance of AbortController() directly after the abort.
The signal needs to be a third parameter going into the Axios
function, and not part of the payload (unlike what you see in
material online).
Side note: In addition, not included here is a debouncer wrapping my query function (in my app), which alongside this AbortController, makes for a good multi-layer management of outgoing/incoming comms with the API server.
(I redacted a bunch of methods/lines that aren't relevant)
export default class MySearch {
constructor() {
// ONE-TIME SETUP
this.payload = null
this.active = {
q: "", // (Query) string e.g. "apples"
facets: {}, // Objects, each with array of options e.g. { 'size': [ '2 x 2 in', '3 x 3 in' ]}, { 'artists': [ 'mike', 'john', 'jane' ] }
page: null, // number e.g. 3
sortBy: null // string, one of: "default" | "newest" | "price_asc" | "price_desc"
}
// Declaring this here. Good/bad?
this.AxiosSearchController = new AbortController()
}
async query() {
return new Promise( async (resolve, reject) => {
// Abort any previous Axios request
this.AxiosSearchController.abort()
// Reinstantiate another instance of AbortController()
this.AxiosSearchController = new AbortController()
this.transformURL()
let requestParams = {
"page": this.active.page,
"sortBy": this.active.sortBy,
"filter": this.active.facets,
}
// Here we tell Axios to associate the request with the controller.
let AxiosSignal = {
signal: this.AxiosSearchController.signal
}
axios.post('/api/search/' + this.active.q, requestParams, AxiosSignal)
.then( response => {
this.payload = response.data
return resolve(response)
})
.catch( error => {
console.error(error)
return reject(error)
})
})
}
}
Where AxiosSearchController is initialized for MySearch depends on if you want multiple instances of the MySearch to keep the same state of search or to maintain their own state of search.
When initialized in the constructor, each instance of MySearch has its own state of search like you have in your snippet.
1. Instance 1 initialized
2. Instance 2 initialized
3. Instance 3 initialized
4. Instance 1 performs request
5. Instance 2 performs request
6. Instance 3 performs request
7. Instance 1 aborts request
8. Instance 2 continues request till fulfillment
9. Instance 3 continues request till fulfillment
When initialized outside of the constructor, all instances of MySearch keep the same state of search.
1. Instance 1 initialized
2. Instance 2 initialized
3. Instance 3 initialized
4. Instance 1 performs request
5. Instance 2 performs request
6. Instance 1 has request aborted
7. Instance 3 performs request
8. Instance 2 has request aborted
Providing the signal property in the params argument is the proper format to set signal for the request for the axios library.
However, when aborting any previous request, AxiosSearchController.signal.aborted gets set to true.
Without resetting this state of the abort controller, you shouldn't be able to make any further requests after the signal is aborted the first time.
You need to initialize AxiosSearchController after aborting request for the previous search.
this.AxiosSearchController.abort();
this.AxiosSearchController = new AbortController();
Related
I have a Chat document that represents a chat between two users. It starts out empty, and eventually looks like this:
// chats/CHAT_ID
{
users: {
USER_ID1: true,
USER_ID2: true
},
lastAddedUser: USER_ID2
}
Each user is connected to a different Cloud Run container via websockets.
I would like to send a welcome message to both users once the second user connected. This message must be sent exactly once.
When a user sends a "connected" message to its websocket, the container performs something like the following:
// Return boolean reflecting whether the current container should emit the welcome message to both users
async addUserToChat(userId) {
// Write operation
await this.chatDocRef.set({ activeUsers: { [userId]: true }, lastAddedUser: userId, { merge: true })
// Read operation
const chatSnap = await this.chatDocRef.get();
const chatData = chatSnap.data();
return chatData.users.length === 2 && chatData.lastAddedUser === userId;
}
And there is a working mechanism that allows container A to send a message to a user connected to container B.
The issue is that sometimes, each container ends up concluding that it is the one that should send the welcome message to both users.
I am unclear as to why that would happen given Firestore's "immediately consistency model" (per this). The only explanation I can think of that allows racing condition is that write operations involving multiple fields are not guaranteed to be atomic. So this:
await this.chatDocRef.set({ activeUsers: { [userId]: true }, lastAddedUser: userId, { merge: true })
actually performs two separate updates for activeUsers and lastAddedUser, opening the possibility for a scenario where after partial update of activeUsers by container A, container B completes the write and read operations before container A overwrites lastAddedUser.
But this sounds wrong.
Can anyone shed light on why racing conditions might occur?
I no longer have racing conditions if I base the logic on the server timestamps instead of the lastAddedUser field.
The document is now simpler:
// chats/CHAT_ID
{
users: {
USER_ID1: true,
USER_ID2: true
}
}
And the function looks like this:
// Return boolean reflecting whether the current container should emit the welcome message to both users
async addUserToChat(userId) {
// Write operation
const writeResult = await this.chatDocRef.set({ activeUsers: { [userId]: true }, { merge: true })
// Read operation
const chatSnap = await this.chatDocRef.get();
const chatData = chatSnap.data();
return chatData.users.length === 2 && writeResult.writeTime.isEqual(chatSnap.updateTime);
}
In other words, the condition for sending the welcome message now becomes: the executing container is the container responsible for the update that resulted in having two users.
While the problem is solved, I am still unclear as to why relying on document data (instead of server metadata) opens up the possibility for racing conditions to occur. If anyone knows the explanation behind this phenomenon, please add an answer and I'll accept it as the solution to this question.
The deprecated #cloudant/cloudant is replaced by ibm-cloud/cloudant package. In former I was using following code snippet
const feed = dummyDB.follow({ include_docs: true, since: 'now'})
feed.on('change', function (change) {
console.log(change)
})
feed.on('error', function (err) {
console.log(err)
})
feed.filter = function (doc, req) {
if (doc._deleted || doc.clusterId === clusterID) {
return true
}
return false
}
Could you share a code for which I can get feed.on event listener similar to above code in new npm package ibm-cloud/cloudant.
There isn't an event emitter for changes in the #ibm-cloud/cloudant package right now. You can emulate the behaviour by either:
polling postChanges (updating the since value after new results) and processing the response result property, which is a ChangesResult. That in turn has a results property that is an array of ChangesResultItem elements, each of which is equivalent to the change argument of the event handler function.
or
call postChangesAsStream with a feed type of continuous and process the stream returned in the response result property, each line of which is a JSON object that follows the structure of ChangesResultItem. In this case you'd also probably want to configure a heartbeat and timeouts.
In both cases you'd need to handle errors to reconnect in the event of network glitches etc.
I am executing 2 update queries in sequential manner. I am using generator function & yield to handle asynchronous behaviour of javascript.
var result = yield db.tasks.update({
"_id": task._id,
"taskLog":{$elemMatch:{"currentApproverRole": vcurrentApproverRole,
"currentApprover": new RegExp(employeeCode, 'i')}}
}, {
$set: {
"taskPendingAt": vnextApproverEmpCode,
"status": vactionTaken,
"lastUpdated": vactionTakenTime,
"lastUpdatedBy": employeeCode,
"shortPin":shortPin,
"workFlowDetails":task.workFlowDetails,
"taskLog.$.reason": reason,
"taskLog.$.actionTakenBy": employeeCode,
"taskLog.$.actionTakenByName": loggedInUser.firstName+" "+loggedInUser.lastName,
"taskLog.$.actionTaken": vactionTaken,
"taskLog.$.actionTakenTime": vactionTakenTime
}
});
var vstatus = vactionTaken;
// Below is the query that is not working properly sometimes
yield db.groupPicnic.update({"gppTaskId": task.workFlowDetails.gppTaskId, "probableParticipantList.employeeCode": task.createdBy},
{
$set: {
'probableParticipantList.$.applicationStatus': vactionTaken
}
})
Second update operation does not execute sometimes (Works 9 out of 10 times). I don't seem to figure out how to handle this issue?
ES6 generators are supposed to provide a simple way for writing iterators.
An iterator is just a sequence of values - like an array, but consumed dynamically and produced lazily.
Currently your code does this:
let imAnUnresolvedPromise = co().next();
// exiting app, promise might not resolve in time
By moving forward and -not- waiting on the promise (assuming your app closes) you can't guarantee that it will execute in time, hence why the unstable behaviour your experiencing.
All you have to change is to wait on the promise to resolve.
let resolveThis = await co().next();
EDIT:
Without async/await syntax you'll have to use nested callbacks to guarantee the correct order, like so:
co().next().then((promiseResolved) => {
co().next().then((promiseTwoResolved) => {
console.log("I'm done")
})
});
I guess I need some type of promise chain, but the syntax eludes me...
Within the same component:
I'm calling:
this.somethingService.getSomethings().then(somethings => this.somethings = somethings);
Then I need to call:
this.otherService.getOthers(this.somethings).then(others => this.others = others);
In the second service call I'm using the result of the first to perform aggregate functions on its content, but its empty when the second call is made, thus the second service returns empty.
How can I get the second service to wait until the first promise has been resolved.
Thanx
Steve
You can chain promises this way:
this.somethingService.getSomethings().then(somethings => {
this.somethings = somethings;
return this.otherService.getOthers(somethings);
}).then(others => {
this.others = others;
});
The second callback will receive the result of the promise returns by the first callback.
I have paged interface. Given a starting point a request will produce a list of results and a continuation indicator.
I've created an observable that is built by constructing and flat mapping an observable that reads the page. The result of this observable contains both the data for the page and a value to continue with. I pluck the data and flat map it to the subscriber. Producing a stream of values.
To handle the paging I've created a subject for the next page values. It's seeded with an initial value then each time I receive a response with a valid next page I push to the pages subject and trigger another read until such time as there is no more to read.
Is there a more idiomatic way of doing this?
function records(start = 'LATEST', limit = 1000) {
let pages = new rx.Subject();
this.connect(start)
.subscribe(page => pages.onNext(page));
let records = pages
.flatMap(page => {
return this.read(page, limit)
.doOnNext(result => {
let next = result.next;
if (next === undefined) {
pages.onCompleted();
} else {
pages.onNext(next);
}
});
})
.pluck('data')
.flatMap(data => data);
return records;
}
That's a reasonable way to do it. It has a couple of potential flaws in it (that may or may not impact you depending upon your use case):
You provide no way to observe any errors that occur in this.connect(start)
Your observable is effectively hot. If the caller does not immediately subscribe to the observable (perhaps they store it and subscribe later), then they'll miss the completion of this.connect(start) and the observable will appear to never produce anything.
You provide no way to unsubscribe from the initial connect call if the caller changes its mind and unsubscribes early. Not a real big deal, but usually when one constructs an observable, one should try to chain the disposables together so it call cleans up properly if the caller unsubscribes.
Here's a modified version:
It passes errors from this.connect to the observer.
It uses Observable.create to create a cold observable that only starts is business when the caller actually subscribes so there is no chance of missing the initial page value and stalling the stream.
It combines the this.connect subscription disposable with the overall subscription disposable
Code:
function records(start = 'LATEST', limit = 1000) {
return Rx.Observable.create(observer => {
let pages = new Rx.Subject();
let connectSub = new Rx.SingleAssignmentDisposable();
let resultsSub = new Rx.SingleAssignmentDisposable();
let sub = new Rx.CompositeDisposable(connectSub, resultsSub);
// Make sure we subscribe to pages before we issue this.connect()
// just in case this.connect() finishes synchronously (possible if it caches values or something?)
let results = pages
.flatMap(page => this.read(page, limit))
.doOnNext(r => this.next !== undefined ? pages.onNext(this.next) : pages.onCompleted())
.flatMap(r => r.data);
resultsSub.setDisposable(results.subscribe(observer));
// now query the first page
connectSub.setDisposable(this.connect(start)
.subscribe(p => pages.onNext(p), e => observer.onError(e)));
return sub;
});
}
Note: I've not used the ES6 syntax before, so hopefully I didn't mess anything up here.