I'm having trouble figuring out how AsyncResource
supports passing of arguments to the bound function.
Details
NodeJS now includes AsyncLocalStorage as an analog to a ThreadLocal in a threaded environment. That is, it provides a storage (usually a key/value map) that can be accessed throughout a call-chain, such as an HTTP request.
This provides a useful pattern for a "request scoped cache" or similar, that would otherwise result in an unwieldy API of passing params around. The cache can just be obtained globally, and nodeJS takes care of providing the unique storage bucket for a given request or call-chain, taking care of managing the set of them behind-the-scenes.
That is until it doesn't. Very occasionally, the async id gets lost along the way. A common case of this is when using EventEmitter extended classes.
Fortunately, for this, there is AsyncResource, which can capture the asyncId in a running callchain, and bind another callback function to that ID, so that the callback function gets the same instance of the cache.
Usage With RxJS
I had a case where an RxJS subscribe method was losing access to the AsyncLocalStorage, even though it had been set up.
I rebound the callback using an AsyncResource
. Here's the puzzling part:
subject.subscribe(asyncResource.runInAsyncScope.bind(asyncResource, async (v) => {
console.log(`observerA: ${v}, with cache value: ${cacheValue}`);
}, null));
When I write the callback as above, arguments are passed into the callback function correctly.
When I write as below, the callback function receives no arguments.
subject.subscribe(asyncResource.runInAsyncScope.bind(asyncResource, async (v) => {
console.log(`observerA: ${v}`);
}));
Question
What is the difference? In the first version we have an additional argument of null
after the callback function. Why does the first version have arguments passed correctly and the latter does not?
Appendix
Here is a test program I used to figure out how to get arguments passed correctly via the AsyncResource
. (The test doesn't reproduce the Async context getting lost, so the RxJS callback would work just as well without the AsyncResource, but in the real-life project, it saves the day).
const server = createServer((req, res) => {
const localStorage = new AsyncLocalStorage<Map<string, any>>();
localStorage.run(localStorage.getStore(), () => {
const store = localStorage.getStore();
store.set("foo", "bar")
const asyncResource = new AsyncResource('request');
subject.subscribe(asyncResource.runInAsyncScope.bind(asyncResource, async (v) => {
const contextHolder = ContextHolder.getInstance()
const cacheValue = contextHolder.localStorage.get<string>("foo")
console.log(`observerA: ${v}, with cache value: ${cacheValue}`);
}, null));
setTimeout(() => {
const store = localStorage.getStore();
store.set("foo", "zzzaaappa")
}, 1500)
subject.next(1);
subject.next(2);
setTimeout(() => {
subject.next(3)
}, 1000)
setTimeout(() => {
subject.next(4)
}, 2000)
setTimeout(() => {
subject.next(5)
}, 3000)
setTimeout(() => {
console.log("$$$$$$$ LONG ONE !!!!!!!!!")
res.writeHead(200)
res.end("Hello there!");
}, 4000)
})
}).listen(3000);
console.log("Running")