3

I'm trying to write a function that performs an asynchronous task and returns a promise while ensuring cleanup occurs after any callbacks are fulfilled. However to do this, it seems I need to know the callback in advance so I can ensure that it happens before the cleanup happens.

Currently, the general structure of the function looks like this:

function doSomethingWithResourceAsync(someParameter, usePreparedResourceCb) {
    var resource = acquireResource(someParameter);
    return prepareResourceAsync(resource)
        .then(usePreparedResourceCb)
        .finally(doCleanup);

    function doCleanup() {
        releaseResource(resource);
    }
}

To call it, I would do this:

doSomethingWithResourceAsync(myParameter, myCallback)
    .then(andSoOn);

function myCallback(proxyObj) {
    return doMagicAsync(proxyObj);
}

This is the only way I can get it to work.

However, I want to write it in a way that I can chain my callback instead while not having to pass around a cleanup callback. So I'd like to call it like this:

function doSomethingWithResourceHopefullyAsync(myParameter) {
    var resource = acquireResource(someParameter);
    return prepareResourceAsync(resource)
        .finally(doCleanup); // uh oh

    function doCleanup() {
        releaseResource(resource);
    }
}

doSomethingWithResourceHopefullyAsync(myParameter)
    .then(myCallback) // too bad, already cleaned up
    .then(andSoOn);

This doesn't work however because the cleanup happens before myCallback gets control and messes things up.

If possible, how can I structure my method to accomplish my goal? Or is what I have the best I can do for this situation?

I have a feeling I could use deferreds to accomplish my goal but I don't know how to set that up to make this work.

The API I'm trying to develop is to be consumed by users who won't necessarily know the intricacies of asynchronous methods so I want to hide that as much as possible.

Jeff Mercado
  • 129,526
  • 32
  • 251
  • 272
  • With Bluebird instead of Q, you could use `using`: https://github.com/petkaantonov/bluebird/blob/master/API.md#resource-management – Denys Séguret Sep 07 '15 at 06:15
  • That might be very useful in the future, thanks for pointing that out to me. – Jeff Mercado Sep 07 '15 at 06:21
  • Very nice figuring out this pattern on your own :) – Benjamin Gruenbaum Sep 07 '15 at 07:13
  • Can you expand your example on what needs to be cleaned up, and how it needs to wait for the promise created by `callback` (or: what would not work in the `callback` if you cleaned up prematurely)? There is a pattern that solves this, but I'd need to go into detail of your problem. – Bergi Sep 07 '15 at 13:58
  • @Bergi: This is part of a Node app. In some instances, it's just for logging. Other instances, it's used to start up a child process (and logging). In my use cases, the child process must be running in order for the callback to work. I'll update when I get the chance. – Jeff Mercado Sep 07 '15 at 17:01
  • So after the `callback` you want to terminate the child process ("cleanup")? And the `callback` is asynchronous and returns a promise? – Bergi Sep 07 '15 at 18:13

1 Answers1

3

What you have is the disposer pattern. Props for figuring it out on your own :)

"Passing" the callback in is necessary because it creates a scope which is what effectively enables the cleanup. You'd know how the callback is "done" by it returning a promise. The fact you need a scope is fundamental, because it's what well... scopes the cleanup. Binding resource allocation to instantiation via scope (RAII) is a useful technique for what you're doing.

I would do something like:

function withResource(handler){
    return acquireResource(). // important to return to chain, like your code
           then(handler).finally(cleanup);
}

Which is effectively what you already have.

As the comments suggest, bluebird's using is a very useful abstraction, it returns disposers which give you a lot of power for cleanup and clean up a lot of type errors. I highly recommend it (though I'm obviously biased).

Community
  • 1
  • 1
Benjamin Gruenbaum
  • 270,886
  • 87
  • 504
  • 504
  • When I mentioned passing around the callback, I wasn't referring to the handler (the `cb()` function in my example), but rather the `doCleanup()` function. I couldn't completely trust that all calling code would remember to call the cleanup function in the handlers always. But it's good to see that you agree with my decisions here. And in retrospect, I think this really is the cleanest way to do it. The name of the actual function already implied that it was going to do something from start to finish. It doesn't make sense to say, "do this to completion, then do this" and expect it to work. – Jeff Mercado Sep 07 '15 at 07:54
  • Exactly, you have to define "start" and "finish" somehow, you might as well do it with a scope which means you don't ever forget to do the cleanup yourself. This is fundamental in many programming languages, it's interesting to see languages integrate the facility in their core - like C#'s `async using` proposal and Python 3.5's `async with`. It'd be great to have this sort of resource management in JavaScript but sadly I don't really think we're there yet - when async/await lands next year (hopefully) we'll have more convincing power :) – Benjamin Gruenbaum Sep 07 '15 at 07:58