Recently I encountered a rather strange scenario, wherein I created two different findAll operations using the js-data-http adapter for a js-data query, only to see two identical network requests emitted. The root of the problem turned out to be that I was creating a single object to hold the query options and reusing it across requests, in combination with the fact that js-data-http mutates its parameters in place during asynchronous operation.
1 2 3 4 |
// This will find posts from user 4 twice! const opts = { force: true }; store.findAll('post', { userId: 2 }, opts); store.findAll('post', { userId: 4 }, opts); |
Here is a brief demonstration of the behaviour. In the fiddle two requests are made to the JSONPlaceholder test API, for posts from different users. When the responses are received, they both contain the posts for the user specified in the second query.
The simple solution is to not use the same object for multiple requests.
1 2 3 |
// This works as expected. store.findAll('post', { userId: 2 }, { force: true }); store.findAll('post', { userId: 4 }, { force: true }); |
Why does this work?
Or more interestingly, why doesn’t the first method work?
Part of the process js-data-http uses is to move the query object passed as the second argument to findAll into a new property of the third argument, options.params. This is because the function that actually makes the network request expects any query parameters to be specified in options.params, at least in part because the query parameter format used in GET requests is not the same as the JSON-formatted query syntax used by js-data. So some amount of transformation is required between what we pass in to js-data and what it sends to the server.
However, because the transformation is done in place, js-data mutates the object that is passed into it. The chain of events goes roughly as follows, due to the asynchronous promise-based nature of the findAll operation.
- An options object is created in your code.
- The first findAll process, Process A, begins, with Query A and Options A.
- The second findAll, Process B, begins, with Query B and Options B.
- Process A moves its query parameters onto the options object, so that options.params contains (a transformed version of) Query A.
- Process B moves its query parameters onto the same options object. Now options.params contains Query B, but it is still also being referenced from Process A!
- Process A makes its network request, based on Query B.
- Process B makes its network request as well, also based on Query B.
- Two responses to the second query are received.
Moral of the story
This turned out to be a very simple solution to a problem that required a very lengthy debugging process, all because I made the incorrect assumption that a third-party library would not have undocumented side effects on the variables that I passed into it. It was complicated by the fact that the side effects were themselves dependent on a different parameter, and of course by the async nature of the operations, but at its heart that was the issue.