-
Notifications
You must be signed in to change notification settings - Fork 842
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
refactor(context-async-hooks): fix eslint warnings #5381
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,13 +19,17 @@ | |
|
||
type Func<T> = (...args: unknown[]) => T; | ||
|
||
// `any` here is intentional for variance, `unknown` won't work | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
type AnyFunc = (this: any, ...args: any[]) => any; | ||
|
||
/** | ||
* Store a map for each event of all original listeners and their "patched" | ||
* version. So when a listener is removed by the user, the corresponding | ||
* patched function will be also removed. | ||
*/ | ||
interface PatchMap { | ||
[name: string]: WeakMap<Func<void>, Func<void>>; | ||
[name: string | symbol]: WeakMap<Func<void>, Func<void>>; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With better type checking after the refactors, TypeScript caught this as Node EventEmitter event types can be symbols (at least according to the types) |
||
} | ||
|
||
const ADD_LISTENER_METHODS = [ | ||
|
@@ -36,6 +40,12 @@ | |
'prependOnceListener' as const, | ||
]; | ||
|
||
// 'addListener' | 'on' | 'once' | 'prependListener' | 'prependOnceListener' | ||
type AddListenerKeys = (typeof ADD_LISTENER_METHODS)[number]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TypeScript gymnastics: const FOO: [string, number, boolean] = [...];
type TString = (typeof FOO)[0];
type TNumber = (typeof FOO)[1];
type TBoolean = (typeof FOO)[2];
type TStringOrNumberOrBoolean = (typeof FOO)[number]; |
||
type AddListener = EventEmitter[AddListenerKeys]; | ||
type RemoveListener = EventEmitter['removeListener']; | ||
type removeAllListeners = EventEmitter['removeAllListeners']; | ||
|
||
export abstract class AbstractAsyncHooksContextManager | ||
implements ContextManager | ||
{ | ||
|
@@ -60,18 +70,21 @@ | |
*/ | ||
bind<T>(context: Context, target: T): T { | ||
if (target instanceof EventEmitter) { | ||
return this._bindEventEmitter(context, target); | ||
return this._bindEventEmitter(context, target) as T; | ||
} | ||
|
||
if (typeof target === 'function') { | ||
return this._bindFunction(context, target); | ||
return this._bindFunction(context, target as T & AnyFunc); | ||
} | ||
return target; | ||
} | ||
|
||
private _bindFunction<T extends Function>(context: Context, target: T): T { | ||
private _bindFunction<T extends AnyFunc>(context: Context, target: T): T { | ||
const manager = this; | ||
const contextWrapper = function (this: never, ...args: unknown[]) { | ||
const contextWrapper = function ( | ||
this: ThisParameterType<T>, | ||
...args: Parameters<T> | ||
): ReturnType<T> { | ||
return manager.with(context, () => target.apply(this, args)); | ||
}; | ||
Object.defineProperty(contextWrapper, 'length', { | ||
|
@@ -80,12 +93,7 @@ | |
writable: false, | ||
value: target.length, | ||
}); | ||
/** | ||
* It isn't possible to tell Typescript that contextWrapper is the same as T | ||
* so we forced to cast as any here. | ||
*/ | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
return contextWrapper as any; | ||
return contextWrapper as T; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We didn't have to cast to However, a cast is necessary here (and technically, unsafe) because And, in general, even if we have to do a completely unrelated unsafe cast, casting into |
||
} | ||
|
||
/** | ||
|
@@ -95,18 +103,17 @@ | |
* @param context the context we want to bind | ||
* @param ee EventEmitter an instance of EventEmitter to patch | ||
*/ | ||
private _bindEventEmitter<T extends EventEmitter>( | ||
context: Context, | ||
ee: T | ||
): T { | ||
private _bindEventEmitter(context: Context, ee: EventEmitter): EventEmitter { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Accepting The reason is that if the input is |
||
const map = this._getPatchMap(ee); | ||
if (map !== undefined) return ee; | ||
this._createPatchMap(ee); | ||
|
||
// patch methods that add a listener to propagate context | ||
ADD_LISTENER_METHODS.forEach(methodName => { | ||
if (ee[methodName] === undefined) return; | ||
ee[methodName] = this._patchAddListener(ee, ee[methodName], context); | ||
const original = ee[methodName]; | ||
if (original) { | ||
ee[methodName] = this._patchAddListener(ee, original, context); | ||
} | ||
}); | ||
// patch methods that remove a listener | ||
if (typeof ee.removeListener === 'function') { | ||
|
@@ -131,15 +138,22 @@ | |
* @param ee EventEmitter instance | ||
* @param original reference to the patched method | ||
*/ | ||
private _patchRemoveListener(ee: EventEmitter, original: Function) { | ||
private _patchRemoveListener( | ||
ee: EventEmitter, | ||
original: RemoveListener | ||
): RemoveListener { | ||
const contextManager = this; | ||
return function (this: never, event: string, listener: Func<void>) { | ||
return function ( | ||
this: ThisParameterType<RemoveListener>, | ||
...args: Parameters<RemoveListener> | ||
): ReturnType<RemoveListener> { | ||
const [event, listener] = args; | ||
const events = contextManager._getPatchMap(ee)?.[event]; | ||
if (events === undefined) { | ||
return original.call(this, event, listener); | ||
if (events) { | ||
const patchedListener = events.get(listener); | ||
args[1] = patchedListener || listener; | ||
} | ||
const patchedListener = events.get(listener); | ||
return original.call(this, event, patchedListener || listener); | ||
return original.apply(this, args); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I changed all the delegating calls in this file to take |
||
}; | ||
} | ||
|
||
|
@@ -149,18 +163,25 @@ | |
* @param ee EventEmitter instance | ||
* @param original reference to the patched method | ||
*/ | ||
private _patchRemoveAllListeners(ee: EventEmitter, original: Function) { | ||
private _patchRemoveAllListeners( | ||
ee: EventEmitter, | ||
original: removeAllListeners | ||
): removeAllListeners { | ||
const contextManager = this; | ||
return function (this: never, event: string) { | ||
const map = contextManager._getPatchMap(ee); | ||
if (map !== undefined) { | ||
if (arguments.length === 0) { | ||
return function ( | ||
this: ThisParameterType<removeAllListeners>, | ||
...args: Parameters<removeAllListeners> | ||
): ReturnType<removeAllListeners> { | ||
const [event] = args; | ||
const events = contextManager._getPatchMap(ee); | ||
if (events) { | ||
if (args.length === 0) { | ||
contextManager._createPatchMap(ee); | ||
} else if (map[event] !== undefined) { | ||
delete map[event]; | ||
} else if (event && events[event]) { | ||
delete events[event]; | ||
} | ||
} | ||
return original.apply(this, arguments); | ||
return original.apply(this, args); | ||
}; | ||
} | ||
|
||
|
@@ -173,11 +194,14 @@ | |
*/ | ||
private _patchAddListener( | ||
ee: EventEmitter, | ||
original: Function, | ||
original: AddListener, | ||
context: Context | ||
) { | ||
): AddListener { | ||
const contextManager = this; | ||
return function (this: never, event: string, listener: Func<void>) { | ||
return function ( | ||
this: ThisParameterType<AddListener>, | ||
...args: Parameters<AddListener> | ||
): ReturnType<AddListener> { | ||
/** | ||
* This check is required to prevent double-wrapping the listener. | ||
* The implementation for ee.once wraps the listener and calls ee.on. | ||
|
@@ -187,27 +211,29 @@ | |
* that detection. | ||
*/ | ||
if (contextManager._wrapped) { | ||
return original.call(this, event, listener); | ||
return original.apply(this, args); | ||
} | ||
let map = contextManager._getPatchMap(ee); | ||
if (map === undefined) { | ||
map = contextManager._createPatchMap(ee); | ||
const [event, listener] = args; | ||
let events = contextManager._getPatchMap(ee); | ||
if (events === undefined) { | ||
events = contextManager._createPatchMap(ee); | ||
Check warning on line 219 in packages/opentelemetry-context-async-hooks/src/AbstractAsyncHooksContextManager.ts
|
||
} | ||
let listeners = map[event]; | ||
let listeners = events[event]; | ||
if (listeners === undefined) { | ||
listeners = new WeakMap(); | ||
map[event] = listeners; | ||
events[event] = listeners; | ||
} | ||
const patchedListener = contextManager.bind(context, listener); | ||
// store a weak reference of the user listener to ours | ||
listeners.set(listener, patchedListener); | ||
args[1] = patchedListener; | ||
|
||
/** | ||
* See comment at the start of this function for the explanation of this property. | ||
*/ | ||
contextManager._wrapped = true; | ||
try { | ||
return original.call(this, event, patchedListener); | ||
return original.apply(this, args); | ||
} finally { | ||
contextManager._wrapped = false; | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using
unknown
here and in the signature ofabstract with()
is probably wrong:https://www.typescriptlang.org/play/?#code/CYUwxgNghgTiAEAzArgOzAFwJYHtXwx2BwAoBKALnlRADcQYBuAKGcigGcP4AVACyyoA5vADezePA4Yo2MPADuWDHwCqqANaocC1AB4AgvBAAPDCFTBuaLTtQBtALoAaeADFjZi1fgkAdAGwQhxUBmTwALwAfPAASiAYyDCoPACeAA4gem5RUSSIqFRulHEJSSkZWTliEpIERKRkLJIAvqyS0rJY8koqBqiphp7mltxQA06uHqYjPv6BMMGh4dGliclpmdm5+YXuJfHrFVvV4nX1xOTN8G1tzCjo2HhIOKRQVNIwgkKuAEZUqGQAFtfgwSr9XhAQOMapJCJcmsw7vxvn5empNNpdPlXoiUcI0co+P1UjicIigA
But I am not touching that in this PR. Fixing it on the
with
signature, in particular, requires some careful analysis, since it's part of the public API. Everything I changed in this PR, in contrast, are private implementation details.