-
Notifications
You must be signed in to change notification settings - Fork 0
Is this proposal strict enough for its use cases? #1
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
Comments
At first I was trying to think through some approach wherein some clever indirection could add some stronger guarantees to these Resources. I was thinking, "what if I couldn't think of many precedents in terms of APIs that strictly required runtime support like this. The obvious one that did come to mind though, was |
Our position is that if you are explicitly calling
That is explicitly not a feature of this proposal and I am not making any such guarantees.
Sure, but if you've chosen to explicitly call |
I think the name |
Not arguing against that, but we are saying that it's not a strict enough guarantee to solve
It's not just manual calls to class Foo {
private stack = new DisposableStack();
example() {
const scope = new AsyncContext.Scope(someSnapshot);
disposable.use(scope);
// Do stuff…
// didn't dispose here!
}
[Symbol.dispose]() {
this.stack[Symbol.dispose]();
}
}
const v = new AsyncContext.Variable();
using f = new Foo();
v.run(1 , () => {
assert.equal(v.get(), 1);
// f is properly using a DispoableStack, but that's not enough
// to prevent a leak.
f.example();
// f's context has leaked out of it's method call.
assert.equal(v.get(), undefined);
}); Ideally, we'd like a way to detect syntatic |
This seems problematic w.r.t. syntax transformation. This sort of feature would (by definition) be untranspilable, making it useful (at best) only in the very-narrow case of transpiling later language features that would depend on it. |
Such a mechanism would have to also support eg |
Someone mentioned a
During the |
Right - which is why "enforcing syntactic usage" won't work, because that would give you a false negative. |
Not quite. It would have to be reset for further nested stacks. |
Not from my point of view. I specifically do not want it to pass, so it's giving a true negative. It is not acceptable to use
Ah, yes, it should reset to the previous value. But still easily implementable with a transform. |
Why isn't that acceptable? It's going to be properly disposed. This really feels like it should just be a linter rule, like no-floating-promises - especially given that there's differing opinions about what level of enforcement is desired. |
@jridgewell and I have been discussing this on Matrix. The kinds of guarantees that using x = new Resource(); to const getResource = () => new Resource();
using x = getResource(); will break their code. The intended use of I have also proposed an alternative approach, where each successive |
I like the Here's the idea: set
In both cases, no new methods need to exist, and you get an unforgeable property you can rely on. This plus two more other optional methods makes async variables entirely implementable in userland, handling assignment through used resources:
Async context could then be implemented like this: Click to expandlet setCurrent
class VariableFork {
#key, #value
constructor(key, value) {
this.#key = key
this.#value = value
}
[Symbol.enter]() {
return this.#key.withValue(this.#value)
}
}
class VariableSet {
#key, #value, #old
#disposed = false
constructor(key, value, old) {
this.#key = key
this.#value = value
this.#old = old
}
[Symbol.fork]() {
return new VariableFork(this.#key, this.#value)
}
[Symbol.dispose]() {
if (this.#disposed) return
this.#disposed = true
setCurrent(this.#key, this.#old)
this.#old = undefined
}
}
delete VariableFork.prototype.constructor
delete VariableSet.prototype.constructor
export class Variable {
#current, #name
static {
setCurrent = (v, c) => {
v.#current = c
}
}
constructor(opts = {}) {
this.#current = opts.default
this.#name = opts.name ?? ""
}
get name() {
return this.#name
}
get() {
return this.#current
}
withValue(value) {
if (!function.using) {
throw new TypeError("This must only be called with `using`")
}
const old = this.#current
this.#current = value
return new VariableSet(this, value, old)
}
run(value, f, ...args) {
using _ = this.withValue(value)
return using f(...args)
}
}
function *snapshotExecutor() {
let value
while (true) {
const {f, args} = yield value
value = f(...args)
}
}
export class Snapshot {
#active = new Variable(false)
#executor = snapshotExecutor()
constructor() {
this.#executor.next()
}
run(f, ...args) {
if (this.#active) return f(...args)
using _ = this.#active.withValue(true)
return this.#executor.next({f, args}).value
}
wrap(f) {
return (...args) => this.run(f, ...args)
}
} |
@dead-claudia That's an interesting and clever idea, but I'm unclear on how (or if) it addresses the {
using _ = getResource();
// ...
} vs. {
using s = new DisposableStack();
s.use(getResource());
// ...
} But if we're tracking Fortunately, the |
What about something like using s = new DisposableStack()
using getResource() with s
// ... (I meant to include constructor calls and related
I, in developing a similar (non-public) system, also considered using scopes, to enable delimited React Effects-like synchronous mutation, but I ultimately tossed it due to concerns around spooky action at a distance. Instead, I just came up with something like this API: AsyncContext.with([
var1.set("foo"),
var2.set("bar"),
// ...
], () => {
// ...
}) |
Uh oh!
There was an error while loading. Please reload this page.
This proposal's name is "strict enforcement of
using
", but while it solves the footgun of forgettingusing
, it is not strict against unintended usage of the disposable API.You might expect that a strict enforcement of an RAII-like pattern would guarantee not only that the
[Symbol.dispose]
method is always called, but that it is called before the current function returns, and that the ordering of[Symbol.enter]
and[Symbol.dispose]
calls across multiple disposables is properly stacked. Since this proposal allows JS code to call the[Symbol.enter]()
and[Symbol.dispose]()
methods, it guarantees none of these properties.As an example use case where these properties matter, in the AsyncContext proposal we were discussing the possibility of a disposable API (see tc39/proposal-async-context#60 and nodejs/node#52065), that would switch the current context when entered and restore the previous one when disposed. However, since the AsyncContext machinery is unaware of function boundaries, the possibility of an entered but non-disposed scope would pollute the caller's context:
Now, it is possible that this is a one-off, or a special case, and that most disposables would benefit from fixing the footgun without needing this level of strictness. But that would need to be looked into.
The text was updated successfully, but these errors were encountered: