-
Notifications
You must be signed in to change notification settings - Fork 29
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
AsyncDisposableStack.use()
cannot dispose Disposable
synchronously
#196
Comments
Well there is your problem. If your |
This can be solved if we provide a synchronous version of import { describe, expect, test, jest } from "@jest/globals";
require("disposablestack/auto");
const SLOT = require('internal-slot');
class ExtendedAsyncDisposableStack extends AsyncDisposableStack {
constructor() {
super();
SLOT.set(this, '[[DisposableState]]', 'pending');
}
useSync(disposable: Disposable): void {
DisposableStack.prototype.use.call(this, disposable);
}
disposeAsync(): Promise<void> {
SLOT.set(this, '[[DisposableState]]', 'disposed');
return super.disposeAsync();
}
move(): AsyncDisposableStack {
SLOT.set(this, '[[DisposableState]]', 'disposed');
return super.move();
}
}
function listen(target: EventTarget, event: string, callback: (e: Event) => void): Disposable {
target.addEventListener(event, callback);
return { [Symbol.dispose]() { target.removeEventListener(event, callback); } };
}
function race(target: EventTarget, event: Event, resolve: () => void): Disposable {
return { [Symbol.dispose]() {
Promise.resolve().then(() => {
target.dispatchEvent(event);
resolve();
});
} };
}
class MyEvent extends Event {
constructor(name: string, public n: number) {
super(name);
}
}
describe("Async race in dispose", () => {
test("ExtendedAsyncDisposableStack with useSync()", async () => {
const log = jest.fn();
await expect(new Promise<void>(async (resolve) => {
await using stack = new ExtendedAsyncDisposableStack();
const target = new EventTarget();
stack.useSync(listen(target, "event", ({ n }: any) => { log(n) }));
stack.useSync(race(target, new MyEvent("event", 2), resolve));
target.dispatchEvent(new MyEvent("event", 1));
})).resolves.not.toThrow();
expect(log.mock.calls).toStrictEqual([[1]]);
});
test("ExtendedAsyncDisposableStack with async use()", async () => {
const log = jest.fn();
await expect(new Promise<void>(async (resolve) => {
await using stack = new ExtendedAsyncDisposableStack();
const target = new EventTarget();
stack.use(listen(target, "event", ({ n }: any) => { log(n) }));
stack.use(race(target, new MyEvent("event", 2), resolve));
target.dispatchEvent(new MyEvent("event", 1));
})).resolves.not.toThrow();
expect(log.mock.calls).toStrictEqual([[1], [2]]);
});
}); |
race's dispose asynchronously dispatches an event, which is a synchronously observable action. The fact it doesn't return a promise to capture that fact is the problem. |
An using stack1 = new AsyncDisposableStack();
// async resources...
const res1 = stack1.use(getSomeAsyncResource1());
const res2 = stack1.use(getSomeAsyncResource2());
// sync resources...
const stack2 = stack1.use(new DisposableStack());
const res3 = stack2.use(getSomeSyncResource3());
const res4 = stack2.use(getSomeSyncResource4());
// more async resources...
const res5 = stack1.use(getSomeAsyncResource5());
... or, you use // async resources...
using stack1 = new AsyncDisposableStack();
const res1 = stack1.use(getSomeAsyncResource1());
const res2 = stack1.use(getSomeAsyncResource2());
// sync resources...
using stack2 = new DisposableStack();
const res3 = stack2.use(getSomeSyncResource3());
const res4 = stack2.use(getSomeSyncResource4());
// more async resources...
await using stack3 = new AsyncDisposableStack();
const res5 = stack3.use(getSomeAsyncResource5());
... Also, as @mhofman said, you are doing an asynchronous thing in |
@mhofman this does beg the question, is it imperative that there is an In essence, given the following: {
await using x = null, y = null, z = null;
}
f(); Is a single |
Note that prior to amending the specification text to mandate an implicit |
Why not? In the implementation for |
One is probably fine, but personally I don't think it's worth the additional complexity. |
We are ok with a specified deterministic collapse of await at the end of block. This would effectively make the number of turns spent at the end of the block dependent on the runtime values of the That said, I do share @bakkot's concern that this would make the spec more complex for reasons I am still unclear about. |
I was looking into this not too long ago. The approach I had considered was to add a slot to IMO, that would result in far clearer spec text than the current approach with respect to |
I will have a PR up shortly for the deterministic collapse of Await, though I do have a question for @mhofman and @erights: Given the following code: {
await using y = syncDisposable, x = asyncDisposable;
happensBefore;
}
happensAfter; Would you consider a single Await for I favor the former, in which {
using y = syncDisposable;
await using x = asyncDisposable;
happensBefore;
}
happensAfter; Here, I could be convinced otherwise, though, since that does result in a subtle difference vs {
await using y = syncDisposable;
happensBefore;
}
happensAfter; Since there is no async disposable in the list, we would introduce an implicit Await between |
I've changed my position here slightly to err on the side of consistency in both directions. Collapse of Example 1try {
using A = syncRes;
await using B = asyncRes;
HAPPENS_BEFORE
} catch { }
HAPPENS_AFTER Here,
Example 2try {
await using A = syncRes, B = asyncRes;
HAPPENS_BEFORE
} catch { }
HAPPENS_AFTER Here,
Example 3try {
await using A = asyncRes, B = syncRes;
HAPPENS_BEFORE
} catch { }
HAPPENS_AFTER Here, only
Note that this means that the above could all occur synchronously if both I've put up #216 to show how this could work. |
What would happen with the following? try {
await using A = syncRes;
HAPPENS_BEFORE
} catch { }
HAPPENS_AFTER My expectation is that HAPPENS_AFTER runs in a new turn, regardless of the nature of the value associated with A. I will need to think further on other collapsing behavior. |
I think this particular one is a problem. There is a syntactic |
This is how Changing that behavior would be wildly inconsistent with the rest of the language. |
Also, the current proposal text operates in this way, as it aligns with (async function () {
try {
await using x = { [Symbol.dispose]() { throw "sync"; } };
Promise.resolve().then(() => { console.log("HAPPENS LATER"); });
console.log("HAPPENS BEFORE");
}
catch {
console.log("HAPPENS AFTER");
}
})(); This would also print
Per Dispose: Which aligns with Evaluation of AwaitExpression: In both cases we use |
The
AsyncDisposableStack.use()
is equivalent toawait using
. But there's no API equivalent tousing
onAsyncDisposableStack
. This can lead to subtle behavior difference if there's async race:The text was updated successfully, but these errors were encountered: