Proto Promise Library

[Edit] It’s now live on the asset store! ProtoPromise | Integration | Unity Asset Store

Hey all, I’m looking for some feedback on my Promise library API and implementation. It’s in a good place now and I’m particularly looking to make sure the API itself is intuitive so that I can lock it down before I put this on the asset store. Of course, feedback on any/all aspects of the code is welcome.

Main features are:

  • Asynchronous wait for operations to complete
  • Reject with any value (uPromise takes any System.Object, boxing value type. RSG takes only exceptions)
  • Catch specific type of rejection or any rejection
  • Cancel pending promise (No other Promise library supports this, Task supports through cancelation token)
  • Catch specific type of cancelation or any cancelation
  • Progress - normalized between 0-1 no matter how many promises are in the chain
  • Convert any yield instruction to a promise (Other Promise libraries provide limited yield instruction support)
  • Optional object pooling (less GC pressure, improving frame rates)
  • Unity-targeted debugging - exceptions in the console have proper unity-parsed stack traces, and the stack traces start at the call into the library (so double-clicking the log will not take you inside the library code). Optionally enable stacktraces where promises are created for better debugging.

I came from uPromise and got frustrated by its API and implementation, so I decided to write my own. I didn’t know about RSG at the time and found out about Tasks after resolve and reject were already done. At that time I wasn’t sure whether I should continue work on this due to built-in tasks that Unity now supports, so I decided to benchmark my library against Tasks, and got very nice results. So I continued my work to this point, adding cancelations and progress.

Features I plan to add:

  • Thread safety (currently it’s only safe on the main thread… ideal for WebGL)

  • Promise.Merge utilizing C#7’s new ValueTuples (Done)

  • De-allocation-free value capture (regular C# delegate variable closures generate a lot of garbage) (Done)

  • async/await support (Done)

  • Task-like concurrent actions

3 Likes

Example usage:

public Promise<Texture2D> DownloadTexture(string url)
{
    var www = UnityWebRequestTexture.GetTexture(url);
    return PromiseYielder.WaitFor(www.SendWebRequest())
        .Then(asyncOperation =>
        {
            if (asyncOperation.webRequest.isHttpError || asyncOperation.webRequest.isNetworkError)
            {
                throw Promise.RejectException(asyncOperation.webRequest.error); // Reject the promise with the string error.
            }
            return ((DownloadHandlerTexture) asyncOperation.webRequest.downloadHandler).texture;
        })
        .Finally(www.Dispose);
}
2 Likes

I added a readme so anyone can get a sense of the API without diving into the code.

Promise.Merge is complete. You can now merge any number of types of promises (Promise.All only allows 1 type).

Also fixed some compile errors when cancelations are disabled.

I haven’t updated in a while since I’ve been pretty busy at my day job, but now…

Capture values is complete! I haven’t given any previous work a version, and this has come pretty far since I started this project, so I gave this pre-release a v0.8.

This update is pretty big, it adds capture values for any delegate (reduced memory and GC pressure compared to closures). It removes a lot of confusing and redundant methods, and quite a bit of cleanup in general. It also brings a few bug fixes along with it, and more extensive unit tests.

From extensive use, I’ve learned that direct cancelation encourages misuse and hard-to-find bugs. Therefore, after I implement async/await support (my next task), I will be changing cancelations to use cancelation source instead of a public Cancel method (similar to how Tasks use cancelation tokens).

v0.9 is now ready!

Main new features:

  • Added async and await support.
  • Enhanced debugging with causality traces and no longer step into library code.
  • Added ContinueWith method to be able to catch a cancelation and continue the promise chain.
  • Reduced memory when Promise.Config.ObjectPooling is set to Internal or All.

See the release notes for detailed changes.

v0.10 is here!

This version is less about new features, more about changing how cancelations work (using cancelation tokens instead of direct cancelation), along with some more intuitive behavior changes and minor optimizations. Given that, it potentially breaks a lot of code that was working in v0.9.

# API changes

  • Added Proto.Promises.{CancelationSource, CancelationToken, CancelationRegistration} structs which can be used for synchronous cancelation callbacks.
  • Added optional CancelationToken parameter to Promise.{Then, Catch, CatchCancelation, ContinueWith, Progress, Sequence, NewDeferred}.
  • Changed Deferreds to structs with an implicit cast to DeferredBase (DeferredBase.ToDeferred(<T>)() to cast back explicitly).
  • Removed DeferredBase.Cancel, replaced with the cancelation token.
  • Removed Promise.{Cancel, ThenDuplicate}.
  • Removed Proto.Logger, replaced it with Promise.Config.WarningHandler.

# Behavior changes

  • Change Deferred.Reject(null) to convert to a NullReferenceException on any T. This means Promise.Catch<T> will never give a null value. This more closely matches normal throw null; behavior.
  • CancelationSource.Cancel(null) now converts cancelation to a non-value.

# Misc

  • Removed recursive type definitions so that Unity 2019.1.14 and older on .Net 4.X scripting runtime version will compile.
  • Removed PROTO_PROMISE_CANCEL_DISABLE preprocessor checks since cancelations no longer slow down normal execution.
  • Renamed asmdefs to include Proto in the names. Removed Utilities.asmdef.

# Known Issues

  • A Promise.Progress callback subscribed to a promise chain where the chain is broken by a cancelation token and continued with Promise.ContinueWith reports incorrect progress (introduced in v 0.9 with Promise.ContinueWith).

See the release notes for more detailed changes.

I will fix the progress bug for the next version, which I plan on making v1.0, and finally getting an official release out! (Threading will wait for post v1.0.)

I know I said I was going to fix the progress bug and make the next version 1.0, but I ran into an issue on an iOS build (thanks to an IL2CPP bug that Unity refuses to fix), so I had to fix that as a higher priority. At the same time, I decided to tackle some other lower-hanging fruits.

So, here’s v0.11.0!

# Optimizations

  • Added object pooling for allocation-free async Promise functions.
  • Removed LitePromises to reduce amount of code.

# Bug Fixes

  • Fixed PromiseMethodBuilders for async Promise functions with the IL2CPP compiler. (Landed in 0.10.2)
  • Fixed causality traces in async Promise functions.
  • Fixed aggregate exception not capturing the unhandled exceptions.

# Misc

  • Better causality traces in general
  • Added implicit cast operators for Promise.Deferred → Promise.DeferredBase and Promise.Deferred → Promise.DeferredBase. (Landed in 0.10.1)

See the release notes for more detailed changes.

v1.0.0 has landed!

Check out the release page which now has a unitypackage available (with a demo inside).
I also added a new github repo to benchmark ProtoPromise against other popular asynchronous libraries.

This project is also going on up the Asset Store, pending review!

# Bug Fixes

  • Fixed PromiseMethodBuilders in non-IL2CPP builds when the TStateMachine is a struct.
  • Fixed various progress bugs.
  • Fixed CancelationToken.Equals(object).

# Behavior changes

  • Added thread checks to make sure the library is only used with one thread (in DEBUG mode only).

# Misc

  • Removed class restriction on PromiseYielder.WaitFor (since there are some structs that can be yielded in Coroutines, like AsyncOperationHandles).

See the release notes for more detailed changes.

It’s now live on the asset store!