I'm trying to track context through the async stack using node async_hooks. It works for most cases, however I have found this use case that I can't think how to resolve:
service.js:
const asyncHooks = require('async_hooks');
class Service {
constructor() {
this.store = {};
this.hooks = asyncHooks.createHook({
init: (asyncId, type, triggerAsyncId) => {
if (this.store[triggerAsyncId]) {
this.store[asyncId] = this.store[triggerAsyncId];
}
},
destroy: (asyncId) => {
delete this.store[asyncId];
},
});
this.enable();
}
async run(fn) {
this.store[asyncHooks.executionAsyncId()] = {};
await fn();
}
set(key, value) {
this.store[asyncHooks.executionAsyncId()][key] = value;
}
get(key) {
const state = this.store[asyncHooks.executionAsyncId()];
if (state) {
return state[key];
} else {
return null;
}
}
enable() {
this.hooks.enable();
}
disable() {
this.hooks.disable();
}
}
module.exports = Service;
service.spec.js
const assert = require('assert');
const Service = require('./service');
describe('Service', () => {
let service;
afterEach(() => {
service.disable();
});
it('can handle promises created out of the execution stack', async () => {
service = new Service();
const p = Promise.resolve();
await service.run(async () => {
service.set('foo');
await p.then(() => {
assert.strictEqual('foo', service.get());
});
});
});
});
This test case will fail because the triggerAsyncId of the promise created when calling next
is the executionAsyncId of the Promise.resolve() call. Which was created outside the current async stack and is a separate context. I can't see any way to marry the next
functions async context with the context it was created in.
I found a solution, which is not perfect, but does work. Wrapping the original promise with Promise.all will resolve to the correct executionAsyncId. But it does rely on the calling code being aware of the promises context.
const assert = require('assert');
const Service = require('./service');
describe('Service', () => {
let service;
afterEach(() => {
service.disable();
});
it('can handle promises created out of the execution stack', async () => {
service = new Service();
const p = Promise.resolve();
await service.run(async () => {
service.set('foo');
await Promise.all([p]).then(() => {
assert.strictEqual('foo', service.get());
});
});
});
});