Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(context): implement withAsync #752 #926

Merged
merged 7 commits into from
Apr 29, 2020
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,19 @@ type PatchedEventEmitter = {
__ot_listeners?: { [name: string]: WeakMap<Func<void>, Func<void>> };
} & EventEmitter;

class Reference<T> {
constructor(private _value: T) {}

set(value: T) {
this._value = value;
return this;
}

get() {
return this._value;
}
}

const ADD_LISTENER_METHODS = [
'addListener' as 'addListener',
'on' as 'on',
Expand All @@ -39,9 +52,7 @@ const ADD_LISTENER_METHODS = [

export class AsyncHooksContextManager implements ContextManager {
private _asyncHook: asyncHooks.AsyncHook;
private _contexts: {
[uid: number]: Context | undefined | null;
} = Object.create(null);
private _contextRefs: Map<number, Reference<Context> | undefined> = new Map();

constructor() {
this._asyncHook = asyncHooks.createHook({
Expand All @@ -52,18 +63,24 @@ export class AsyncHooksContextManager implements ContextManager {
}

active(): Context {
return (
this._contexts[asyncHooks.executionAsyncId()] || Context.ROOT_CONTEXT
);
const ref = this._contextRefs.get(asyncHooks.executionAsyncId());
return ref === undefined ? Context.ROOT_CONTEXT : ref.get();
}

with<T extends (...args: unknown[]) => ReturnType<T>>(
context: Context,
fn: T
): ReturnType<T> {
const uid = asyncHooks.executionAsyncId();
const oldContext = this._contexts[uid];
this._contexts[uid] = context;
let ref = this._contextRefs.get(uid);
let oldContext: Context | undefined = undefined;
if (ref === undefined) {
ref = new Reference(context);
this._contextRefs.set(uid, ref);
} else {
oldContext = ref.get();
ref.set(context);
}
try {
return fn();
} catch (err) {
Expand All @@ -72,7 +89,34 @@ export class AsyncHooksContextManager implements ContextManager {
if (oldContext === undefined) {
this._destroy(uid);
} else {
this._contexts[uid] = oldContext;
ref.set(oldContext);
}
}
}

async withAsync<T extends Promise<any>, U extends (...args: unknown[]) => T>(
vmarchaud marked this conversation as resolved.
Show resolved Hide resolved
context: Context,
fn: U
): Promise<T> {
const uid = asyncHooks.executionAsyncId();
let ref = this._contextRefs.get(uid);
let oldContext: Context | undefined = undefined;
if (ref === undefined) {
ref = new Reference(context);
this._contextRefs.set(uid, ref);
} else {
oldContext = ref.get();
ref.set(context);
}
try {
return await fn();
} catch (err) {
throw err;
} finally {
if (oldContext === undefined) {
this._destroy(uid);
} else {
ref.set(oldContext);
}
}
}
Expand All @@ -97,7 +141,7 @@ export class AsyncHooksContextManager implements ContextManager {

disable(): this {
this._asyncHook.disable();
this._contexts = {};
this._contextRefs.clear();
return this;
}

Expand Down Expand Up @@ -232,7 +276,10 @@ export class AsyncHooksContextManager implements ContextManager {
* @param uid id of the async context
*/
private _init(uid: number) {
this._contexts[uid] = this._contexts[asyncHooks.executionAsyncId()];
const ref = this._contextRefs.get(asyncHooks.executionAsyncId());
if (ref !== undefined) {
this._contextRefs.set(uid, ref);
}
}

/**
Expand All @@ -241,6 +288,6 @@ export class AsyncHooksContextManager implements ContextManager {
* @param uid uid of the async context
*/
private _destroy(uid: number) {
delete this._contexts[uid];
this._contextRefs.delete(uid);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,172 @@ describe('AsyncHooksContextManager', () => {
});
});

describe('.withAsync()', () => {
it('should run the callback', async () => {
let done = false;
await contextManager.withAsync(Context.ROOT_CONTEXT, async () => {
done = true;
});

assert.ok(done);
});

it('should run the callback with active scope', async () => {
const test = Context.ROOT_CONTEXT.setValue(key1, 1);
await contextManager.withAsync(test, async () => {
assert.strictEqual(contextManager.active(), test, 'should have scope');
});
});

it('should run the callback (when disabled)', async () => {
contextManager.disable();
let done = false;
await contextManager.withAsync(Context.ROOT_CONTEXT, async () => {
done = true;
});

assert.ok(done);
});

it('should rethrow errors', async () => {
contextManager.disable();
let done = false;
const err = new Error();

try {
await contextManager.withAsync(Context.ROOT_CONTEXT, async () => {
throw err;
});
} catch (e) {
assert.ok(e === err);
done = true;
}

assert.ok(done);
});

it('should finally restore an old scope', async () => {
const scope1 = '1' as any;
const scope2 = '2' as any;
let done = false;

await contextManager.withAsync(scope1, async () => {
assert.strictEqual(contextManager.active(), scope1);
await contextManager.withAsync(scope2, async () => {
assert.strictEqual(contextManager.active(), scope2);
done = true;
});
assert.strictEqual(contextManager.active(), scope1);
});

assert.ok(done);
});
});

describe('.withAsync/with()', () => {
it('with() inside withAsync() should correctly restore context', async () => {
const scope1 = '1' as any;
const scope2 = '2' as any;
let done = false;

await contextManager.withAsync(scope1, async () => {
assert.strictEqual(contextManager.active(), scope1);
contextManager.with(scope2, () => {
assert.strictEqual(contextManager.active(), scope2);
done = true;
});
assert.strictEqual(contextManager.active(), scope1);
});

assert.ok(done);
});

it('withAsync() inside with() should correctly restore conxtext', done => {
const scope1 = '1' as any;
const scope2 = '2' as any;

contextManager.with(scope1, async () => {
assert.strictEqual(contextManager.active(), scope1);
await contextManager.withAsync(scope2, async () => {
assert.strictEqual(contextManager.active(), scope2);
});
assert.strictEqual(contextManager.active(), scope1);
return done();
});
assert.strictEqual(contextManager.active(), Context.ROOT_CONTEXT);
});

it('not awaited withAsync() inside with() should not restore context', done => {
const scope1 = '1' as any;
const scope2 = '2' as any;
let _done: boolean = false;

contextManager.with(scope1, () => {
assert.strictEqual(contextManager.active(), scope1);
contextManager
.withAsync(scope2, async () => {
assert.strictEqual(contextManager.active(), scope2);
})
.then(() => {
assert.strictEqual(contextManager.active(), scope1);
_done = true;
});
// in this case the current scope is 2 since we
// didnt waited the withAsync call
assert.strictEqual(contextManager.active(), scope2);
vmarchaud marked this conversation as resolved.
Show resolved Hide resolved
setTimeout(() => {
assert.strictEqual(contextManager.active(), scope1);
assert(_done);
return done();
}, 100);
});
assert.strictEqual(contextManager.active(), Context.ROOT_CONTEXT);
});
vmarchaud marked this conversation as resolved.
Show resolved Hide resolved

it('withAsync() inside a setTimeout inside a with() should correctly restore context', done => {
const scope1 = '1' as any;
const scope2 = '2' as any;

contextManager.with(scope1, () => {
assert.strictEqual(contextManager.active(), scope1);
setTimeout(() => {
assert.strictEqual(contextManager.active(), scope1);
contextManager
.withAsync(scope2, async () => {
assert.strictEqual(contextManager.active(), scope2);
})
.then(() => {
assert.strictEqual(contextManager.active(), scope1);
return done();
});
}, 5);
assert.strictEqual(contextManager.active(), scope1);
});
assert.strictEqual(contextManager.active(), Context.ROOT_CONTEXT);
});

it('with() inside a setTimeout inside withAsync() should correctly restore context', done => {
const scope1 = '1' as any;
const scope2 = '2' as any;

contextManager
.withAsync(scope1, async () => {
assert.strictEqual(contextManager.active(), scope1);
setTimeout(() => {
assert.strictEqual(contextManager.active(), scope1);
contextManager.with(scope2, () => {
assert.strictEqual(contextManager.active(), scope2);
return done();
});
}, 5);
assert.strictEqual(contextManager.active(), scope1);
})
.then(() => {
assert.strictEqual(contextManager.active(), scope1);
});
});
});

describe('.bind(function)', () => {
it('should return the same target (when enabled)', () => {
const test = { a: 1 };
Expand Down