Preferences

Oh my word.

    const defer = f => ({ [Symbol.dispose]: f })

    using defer(() => cleanup())
That only just occurred to me. To everybody else who finds it completely obvious, "well done" but it seemed worthy of mention nonetheless.

Note that depending on use case, it may be preferrable to use `DisposableStack` and `AsyncDisposableStack` which are part of the `using` proposal and have built-in support for callback registration.

This is notably necessary for scope-bridging and conditional registration as `using` is block-scoped so

    if (condition) {
        using x = { [Symbol.dispose]: cleanup }
    } // cleanup is called here
But because `using` is a variant of `const` which requires an initialisation value which it registers immediately this will fail:

    using x; // SyntaxError: using missing initialiser
    if (condition) {
        x = { [Symbol.dispose]: cleanup };
    }
and so will this:

    using x = { [Symbol.dispose]() {} };
    if (condition) {
        // TypeError: assignment to using variable
        x = { [Symbol.dispose]: cleanup }
    }
Instead, you'd write:

    using x = new DisposableStack;
    if (condition) {
        x.defer(cleanup)
    }
Similarly if you want to acquire a resource in a block (conditionally or not) but want the cleanup to happen at the function level, you'd create a stack at the function toplevel then add your disposables or callbacks to it as you go.
What is the purpose of DisposableStack.move()? Can it be used to transfer the collected .defer() callbacks entirely out of the current scope, e.g. up the call stack? Probably would be easier to pass DisposableStack as an argument to stack all .defer() callbacks in the caller's context?
Yup, or to transfer them anywhere else. One use case is for classes which allocate resources in their constructor:

    class Connector {
      constructor() {
        using stack = new DisposableStack;
        
        // Foo and Bar are both disposable
        this.foo = stack.use(new Foo());
        this.bar = stack.use(new Bar());
        this.stack = stack.move();
      }
      
      [Symbol.dispose]() {
        this.stack.dispose();
      }
    }
In this example you want to ensure that if the constructor errors partway through then any resources already allocated get cleaned up, but if it completes successfully then resources should only get cleaned up once the instance itself gets cleaned up.
> Probably would be easier to pass DisposableStack as an argument to stack all .defer() callbacks in the caller's context?

The problem in that case if if the current function can acquire disposables then error:

    function thing(stack) {
        const f = stack.use(new File(...));
        const g = stack.use(new File(...));
        if (something) {
            throw new Error
        }
        // do more stuff
        return someObject(f, g);
    }
rather than be released on exit, the files will only be released when the parent decides to dispose of its stack.

So what you do instead is use a local stack, and before returning successful control you `move` the disposables from the local stack to the parents', which avoids temporal holes:

    function thing(stack) {
        const local = new DisposableStack;
        const f = local.use(new File(...));
        const g = local.use(new File(...));
        if (something) {
            throw new Error
        }
        // do more stuff

        stack.use(local.move());
        return someObject(f, g);
    }
Although in that case you would probably `move` the stack into `someObject` itself as it takes ownership of the disposables, and have the caller `using` that:

    function thing() {
        const local = new DisposableStack;
        const f = local.use(new File(...));
        const g = local.use(new File(...));
        if (something) {
            throw new Error
        }
        // do more stuff

        return someObject(local.move(), f, g);
    }
In essence, `DisposableStack#move` is a way to emulate RAII's lifetime-based resource management, or the error-only defers some languages have.
(const local should be using local in the snippets).
I wrote about this last year, it's one of my favourite bits of this spec: https://jonathan-frere.com/posts/disposables-in-javascript/#...

TL;DR: the problem if you just pass the DisposableStack that you're working with is that it's either a `using` variable (in which case it will be disposed automatically when your function finishes, even if you've not actually finished with the stack), or it isn't (in which case if an error gets thrown while setting up the stack, the resources won't be disposed of properly).

`.move()` allows you to create a DisposableStack that's a kind of sacrificial lamb: if something goes wrong, it'll dispose of all of its contents automatically, but if nothing goes wrong, you can empty it and pass the contents somewhere else as a safe operation, and then let it get disposed whenever.

Just like golang. Nice.

This item has no comments currently.

Keyboard Shortcuts

Story Lists

j
Next story
k
Previous story
Shift+j
Last story
Shift+k
First story
o Enter
Go to story URL
c
Go to comments
u
Go to author

Navigation

Shift+t
Go to top stories
Shift+n
Go to new stories
Shift+b
Go to best stories
Shift+a
Go to Ask HN
Shift+s
Go to Show HN

Miscellaneous

?
Show this modal