Closed rixtox closed 1 year ago
A solution probably being the least intrusive to the current design, is to introduce a TryResult
type:
interface TryResult {
hasError: boolean;
error: any; // thrown value
}
interface Disposable {
[Symbol.dispose](result: TryResult): void;
}
And the semantics becomes:
{
const $$try = { stack: [], hasError: false, error: undefined };
try {
... // (1)
const x = expr1;
if (x !== null && x !== undefined) {
const $$dispose = x[Symbol.dispose];
if (typeof $$dispose !== "function") {
throw new TypeError();
}
$$try.stack.push({ value: x, dispose: $$dispose });
}
const y = expr2;
if (y !== null && y !== undefined) {
const $$dispose = y[Symbol.dispose];
if (typeof $$dispose !== "function") {
throw new TypeError();
}
$$try.stack.push({ value: y, dispose: $$dispose });
}
... // (2)
}
catch ($$error) {
$$try.error = $$error;
$$try.hasError = true;
}
finally {
while ($$try.stack.length) {
const { value: $$expr, dispose: $$dispose } = $$try.stack.pop();
try {
$$dispose.call($$expr, { hasError: $$try.hasError, error: $$try.error });
}
catch ($$error) {
$$try.error = $$try.hasError ? new SuppressedError($$error, $$try.error) : $$error;
$$try.hasError = true;
}
}
if ($$try.hasError) {
throw $$try.error;
}
}
}
I withdraw my opinion of passing parent completion result to the dispose method. Exposing the dispose method made it tempting to use Disposable as a cancellation mechanism, while in principle it should not be entangled with the execution order and interruption of an execution scope.
Exposing the dispose method also exposed transferable ownership (or control) of the resource. In general, I don't think that's a good idea at all. Ownership should be declared, not assigned.
Cancellation on the other hand, is assigning a controlling signal to a running task. Cancellation cannot be declared.
The current design of the Disposable interface is mixing these concepts, which made it tempting for people to abuse it for things it shouldn't do.
I have made a counter-proposal in #159. Closing this issue.
I'm looking at this problem with the Cancellation API in mind. I know it's probably not the scope for this proposal but I think we should probably think ahead of possible interaction with a future Cancellation design.
This means a Disposable can not only be some kind of resource, but it can also be some kind of tasks. If it's a task, it would probably be interested in the reason for it to be disposed, which means to distinguish between disposal because of normal completion, and disposal because of abruption.
Consider the following example that leverage generator abruption as a cancellation mechanism.
When the parent got abrupt with an error, the child task would got disposed by
task[Symbol.dispose]()
. Notice that the task would have no way to distinguish it with a dispose call whenfoo()
is returning normally without error.A suggestion is to pass in the parent's thrown error to the bond
task[Symbol.dispose](err)
call, orundefined
if it the parent is normally completed. However this would have a corner case ifundefined
was thrown. I think we should look into this use case anyway.I think a comparable design is the Tomb Golang package that does
tomb.kill(reason)
and the reason would be propagated to bound cleanup receivers.