Promises
Details about the A+ promise implementation in uMod.
tl;dr: I'm already familiar with promises in JS
uMod Promises are designed to work just like JS promises with a couple of notable differences.
- Promise chains should end with a call to a terminator method; either
Done,FailorComplete.They operate identically toThen,CatchandFinally, respectively. Avoiding unnecessary promises, or using terminator methods, is an optimization that reduces allocations. - The
Fail/Catchmethod is necessary to recover from exceptions occuring within other promise callbacks.
Introduction
A promise represents the result of an asynchronous operation. In most cases promises are created or pooled externally and developers will attach callbacks to already existing promises.
For example, consider asynchronously loading a data file that contains a single integer...
Action<int> successCallback = (int value) =>
{
// The file was loaded and its content deserialized on a separate background thread
};
Action<Exception> errorCallback = (Exception exception) =>
{
// Something went wrong
Logger.Report("Unable to load file", exception);
};
var promise = Files.ReadObject<int>("path/to/file.dat");
promise.Done(successCallback, errorCallback);
In the above example, the file file.dat will be loaded into memory and deserialized in a background thread. Once this process is complete, the successCallback will be invoked (with the result of the fulfilled promise), or the errorCallback will be invoked (in the case of an exception).
Terminology
Fulfilled- A fulfilled promise is a promise that has successfully finished.Rejected- A rejected promise is a promise that has finished with a failed state.Settled- A settled promise is a promise that either fulfilled or rejected.Pending- A pending promise is a promise that has not settled.Resolved- A resolved promise is a promise that may not have yet been settled but its outcome has already been determined. This usually means the outcome of this promise will be determined by the outcome of another promise.
Constructing a promise
In the example below, when a player runs the command /test, a promise is created which will resolve when a repeating timer has completed execution.
[Command("test")]
void TestCommand(IPlayer player)
{
MyTimerPromise(player)
.Done(delegate()
{
player.Reply("Timer executed 5 times over 25 seconds");
});
}
IPromise MyTimerPromise(IPlayer player)
{
Promise promise = new Promise();
int count = 1;
int recurrences = 5;
timer.Repeat(5f, recurrences, delegate()
{
player.Reply($"Timer executed: {count}");
if (count == recurrences)
{
// Timer finished
promise.Resolve();
}
count++;
});
return promise;
}
The above code is an example of deferring a callback until the promised (typically asynchronous) operation is complete and will print...
Timer executed: 1
Timer executed: 2
Timer executed: 3
Timer executed: 4
Timer executed: 5
Timer executed 5 times over 25 seconds
Utility functions
There following utility function available to allow easily handling multiple promise:
Promise.All- Wait for all given promises to fulfill.Promise.AllSettled- Wait for all given promises to settle (fulfill or reject).Promise.Any- Wait for the first of given promises to fulfill.Promise.Race- Wait for the first of given promises to settle (fulfill or reject).
The most commonly used of these is Promise.All which can be used like so:
var promise1 = Files.ReadObject<int>("path/to/file1.dat");
var promise2 = Files.ReadObject<int>("path/to/file2.dat");
Promise.All(promise1, promise2).Done(successCallback, errorCallback);
Note: This is more performant than chaining the two promises together as this allows the two promise to execute in parallel.
Chaining
Promises may be chained together into a series of promises which invoke two or more asynchronous operations. Chained promises are called in order and only upon completion of the previous promise.
For example, we may want to perform a web request using the data received from the first async operation.
promise
.Then((int id) =>
{
return Web.Get($"https://httpbin.org/anything/{id}");
})
.Then((WebResponse response) =>
{
if (response.StatusCode == 200)
{
Logger.Info(response.ReadAsString());
}
})
.Fail(errorCallback);
When chaining promises only one error handler is required near the end of the chain. This will catch any errors that happen before it in the chain. Additional error handles can be added up the chain via the Catch method if necessary to recover from earlier callback errors.
Chain Tterminators
There are 3 promise chain terminator methods: Done, Fail and Complete.These are counterparts to their non-terminator methods Then, Catch and Finally respectively.
It is highly recommend to always end a promise chain with a terminator method.
A promise chain terminator is a method designed to be called at the end of a promise chain. Without a final call to one of these methods, if an exception is thrown in one of the promises in the chain (i.e. a promise rejects) and it isn't handled via an errorCallback the exception is quietly ignored.
Using a terminator method indicates that the promise is no longer being used and is safe to dispose of (or free from memory).
This means that after the call it is no longer safe to reference any promises in the chain; doing so may result in a ObjectDisposedException or in the worst case, using a promise that is already being reused elsewhere.
Note: As long as it can be guaranteed that the promise has not yet settled (i.e. it's still pending) then it is safe to keep using it after calling a terminator method but it is not recommended.
Chain transformation
Chained promises may transform the promise parameter and yield it to the next promise in the chain (or the terminator).
For example, simply perform arithmetic using an integer promise and print the result:
promise
.Then(delegate(int result)
{
return result + 1;
})
.Then(delegate(int newResult)
{
Logger.Info($"Result was {newResult}");
}).Fail(failCallback);
Change the type by converting/casting the promise result...
promise.Then(delegate(string result)
{
if (int.TryParse(result, out int newResult))
{
return newResult;
}
throw new InvalidArgumentException("result");
}).Then(delegate(int result)
{
// do something with integer
});