-
-
Notifications
You must be signed in to change notification settings - Fork 31.9k
meta: add guidelines for introduction of ERM support #58526
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
base: main
Are you sure you want to change the base?
Conversation
Review requested:
|
626622b
to
51d96c1
Compare
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.
Looks good on the whole, but it's worth pointing out that the role of disposability as stated here – basically a "sweeper" that avoids having to manually perform any last-ditch teardown (such as would occur in a finally
block), but which still puts the onus on the user to explicitly invoke graceful disposal under normal execution – doesn't tally with the indicative cases from the tc39 proposal, which imply an environment wherein one doesn't need to explicitly close one's own disposable resources at all, and can just defer to the ERM disposer under both normal and abnormal conditions.
I don't have any strong feelings here, but probably warrants discussion.
@Renegade334 I think the way I would put it is that very few resources care about whether you are cleaning up because of a thrown exception or not. Those which do require special handling, but it's not something most APIs should need to think about. |
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.
Unless I missed it, this document doesn't really cover creating handles like in #58453, i.e. these:
return {
[SymbolDispose]: () => this.removeListener(type, listener),
};
Since we don't have using void
yet and disposables are not always grouped by DisposableStack
s, these objects usually will be exposed in userspace and have some user-defined names.
Should such objects be just this? Maybe they should be instances of some DisposableEntity class, implementing disposable interface? Or maybe they should be null-prototyped? Or maybe they should have Symbol.toPrimitive
or kInspect
that would help identifying what this object is? Or we should also add non-symbol dispose function (if so, we probably should come up with a generic name for all such objects)?
Personally I am happy with anonymous objects.
Don't see any reason for this - null prototypes make sense when you might have unknown keys (like
Definitely not
I do think there should be a string named dispose function. I don't think it should have a generic name - the disposal action is fundamentally different for different kinds of things (sometimes it's |
e77f4c7
to
17a85e3
Compare
17a85e3
to
e1f1ffb
Compare
Note: following the discussion at #58526 (comment), the filename still stands at |
{ using myDisposable = new MyResource(); } | ||
``` | ||
|
||
Or even fully anonymous objects: |
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.
I'd disagree with recommending returning an anonymous objects in an API design guideline. We should recommend structured API to improve comprehensiveness and discoverablity.
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.
@legendecas The problem is that lots of APIs need a bespoke disposable which isn't used for anything else, and adding a new class for every such API ends up littering lots of little classes all over the place. See e.g. #58516 for such an API. I think anonymous objects are better than having a separate class for each such API.
(But if you disagree, leave a comment on that PR as well.)
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.
This is likely something that can be best handled in documentation than code. For instance, in code we can have it be a fully anonymous object:
return {
dispose() { ... },
[Symbol.dispose]() { this.dispose(); }
}
While in documentation is can at least appear to be a named interface:
### `foo()`
* Returns {Disposable}
### `Disposable`
#### `disposable.dispose()`
#### `disposable[Symbol.dispose]`
So I'd suggest that we're talking about a documentation difference here, not necessarily a coding difference.
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.
I think the question is more like how user anticipates with a disposable API. If an API returns an object, it should be easy to know what users can do with the object.
#58516 is an example that there are more properties than the documented path
on the returned anonymous object, like remove()
. It is not ideal to just say "the returned object has property A, B, and C". We should document every public API and its behavior clearly if possible.
Regarding "adding a new class for every such API ends up littering lots of little classes all over the place", IMO this is a question on if it is worthwhile to add a new API to add disposable support. If an API already returns an object, there is no need to add a new disposable class for it.
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.
LGTM
Can you add an example about asyncDispose
as well?
Co-authored-by: Gerhard Stöbich <[email protected]>
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.
This document is very informative. Excited to see more of this implemented throughout core
criteria is not met, then the disposer is actually a synchronous disposer in | ||
disguise, and will block the execution thread until it returns; such a | ||
disposer should instead be declared using `Symbol.dispose`. | ||
7. Avoid, as much as possible, using both `Symbol.dispose` and `Symbl.asyncDispose` |
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.
This came up in an undici PR, and is also probably worth stating – again, wording flexible:
7. Avoid, as much as possible, using both `Symbol.dispose` and `Symbl.asyncDispose` | |
7. Because the disposal process is strictly ordered, there is an intrinsic | |
expectation that all tasks performed by a single disposer are fully complete | |
at the point that the disposer returns. This means, for example, that | |
"callback-style" APIs must not be invoked within a disposer, unless they are | |
promisified and awaited. Any Promise created within a disposer must be | |
awaited, to ensure its resolution prior to the disposer returning. | |
8. Avoid, as much as possible, using both `Symbol.dispose` and `Symbol.asyncDispose` |
Stemming from discussions in #58516 , this seeks to add guidance for introducing explicit resource management support into existing Node.js APIs. This is mean to be discussed and evolved so please weigh in.
/cc @nodejs/tsc @nodejs/collaborators @bakkot