using RSG.Promises;
using System;
using System.Collections.Generic;
using System.Linq;
using RSG.Exceptions;
using UIWidgets.ui;
namespace RSG
{
///
/// Implements a C# promise.
/// https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise
///
public interface IPromise
{
///
/// Gets the id of the promise, useful for referencing the promise during runtime.
///
int Id { get; }
///
/// Set the name of the promise, useful for debugging.
///
IPromise WithName(string name);
///
/// Completes the promise.
/// onResolved is called on successful completion.
/// onRejected is called on error.
///
void Done(Action onResolved, Action onRejected);
///
/// Completes the promise.
/// onResolved is called on successful completion.
/// Adds a default error handler.
///
void Done(Action onResolved);
///
/// Complete the promise. Adds a default error handler.
///
void Done();
///
/// Handle errors for the promise.
///
IPromise Catch(Action onRejected);
///
/// Handle errors for the promise.
///
IPromise Catch(Func onRejected);
///
/// Add a resolved callback that chains a value promise (optionally converting to a different value type).
///
IPromise Then(Func> onResolved);
///
/// Add a resolved callback that chains a non-value promise.
///
IPromise Then(Func onResolved);
///
/// Add a resolved callback.
///
IPromise Then(Action onResolved);
///
/// Add a resolved callback and a rejected callback.
/// The resolved callback chains a value promise (optionally converting to a different value type).
///
IPromise Then(
Func> onResolved,
Func> onRejected
);
///
/// Add a resolved callback and a rejected callback.
/// The resolved callback chains a non-value promise.
///
IPromise Then(Func onResolved, Action onRejected);
///
/// Add a resolved callback and a rejected callback.
///
IPromise Then(Action onResolved, Action onRejected);
///
/// Add a resolved callback, a rejected callback and a progress callback.
/// The resolved callback chains a value promise (optionally converting to a different value type).
///
IPromise Then(
Func> onResolved,
Func> onRejected,
Action onProgress
);
///
/// Add a resolved callback, a rejected callback and a progress callback.
/// The resolved callback chains a non-value promise.
///
IPromise Then(Func onResolved, Action onRejected, Action onProgress);
///
/// Add a resolved callback, a rejected callback and a progress callback.
///
IPromise Then(Action onResolved, Action onRejected, Action onProgress);
///
/// Return a new promise with a different value.
/// May also change the type of the value.
///
IPromise Then(Func transform);
///
/// Chain an enumerable of promises, all of which must resolve.
/// Returns a promise for a collection of the resolved results.
/// The resulting promise is resolved when all of the promises have resolved.
/// It is rejected as soon as any of the promises have been rejected.
///
IPromise> ThenAll(Func>> chain);
///
/// Chain an enumerable of promises, all of which must resolve.
/// Converts to a non-value promise.
/// The resulting promise is resolved when all of the promises have resolved.
/// It is rejected as soon as any of the promises have been rejected.
///
IPromise ThenAll(Func> chain);
///
/// Takes a function that yields an enumerable of promises.
/// Returns a promise that resolves when the first of the promises has resolved.
/// Yields the value from the first promise that has resolved.
///
IPromise ThenRace(Func>> chain);
///
/// Takes a function that yields an enumerable of promises.
/// Converts to a non-value promise.
/// Returns a promise that resolves when the first of the promises has resolved.
/// Yields the value from the first promise that has resolved.
///
IPromise ThenRace(Func> chain);
///
/// Add a finally callback.
/// Finally callbacks will always be called, even if any preceding promise is rejected, or encounters an error.
/// The returned promise will be resolved or rejected, as per the preceding promise.
///
IPromise Finally(Action onComplete);
///
/// Add a callback that chains a non-value promise.
/// ContinueWith callbacks will always be called, even if any preceding promise is rejected, or encounters an error.
/// The state of the returning promise will be based on the new non-value promise, not the preceding (rejected or resolved) promise.
///
IPromise ContinueWith(Func onResolved);
///
/// Add a callback that chains a value promise (optionally converting to a different value type).
/// ContinueWith callbacks will always be called, even if any preceding promise is rejected, or encounters an error.
/// The state of the returning promise will be based on the new value promise, not the preceding (rejected or resolved) promise.
///
IPromise ContinueWith(Func> onComplete);
///
/// Add a progress callback.
/// Progress callbacks will be called whenever the promise owner reports progress towards the resolution
/// of the promise.
///
IPromise Progress(Action onProgress);
}
///
/// Interface for a promise that can be rejected.
///
public interface IRejectable
{
///
/// Reject the promise with an exception.
///
void Reject(Exception ex);
}
///
/// Interface for a promise that can be rejected or resolved.
///
public interface IPendingPromise : IRejectable
{
///
/// ID of the promise, useful for debugging.
///
int Id { get; }
///
/// Resolve the promise with a particular value.
///
void Resolve(PromisedT value);
///
/// Report progress in a promise.
///
void ReportProgress(float progress);
}
///
/// Specifies the state of a promise.
///
public enum PromiseState
{
Pending, // The promise is in-flight.
Rejected, // The promise has been rejected.
Resolved // The promise has been resolved.
};
///
/// Implements a C# promise.
/// https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise
///
public class Promise : IPromise, IPendingPromise, IPromiseInfo
{
///
/// The exception when the promise is rejected.
///
private Exception rejectionException;
///
/// The value when the promises is resolved.
///
private PromisedT resolveValue;
///
/// Error handler.
///
private List rejectHandlers;
///
/// Progress handlers.
///
private List progressHandlers;
///
/// Completed handlers that accept a value.
///
private List> resolveCallbacks;
private List resolveRejectables;
///
/// ID of the promise, useful for debugging.
///
public int Id { get { return id; } }
private readonly int id;
///
/// Name of the promise, when set, useful for debugging.
///
public string Name { get; private set; }
///
/// Tracks the current state of the promise.
///
public PromiseState CurState { get; private set; }
public Promise()
{
this.CurState = PromiseState.Pending;
this.id = Promise.NextId();
if (Promise.EnablePromiseTracking)
{
Promise.PendingPromises.Add(this);
}
}
public Promise(Action, Action> resolver)
{
this.CurState = PromiseState.Pending;
this.id = Promise.NextId();
if (Promise.EnablePromiseTracking)
{
Promise.PendingPromises.Add(this);
}
try
{
resolver(Resolve, Reject);
}
catch (Exception ex)
{
Reject(ex);
}
}
///
/// Add a rejection handler for this promise.
///
private void AddRejectHandler(Action onRejected, IRejectable rejectable)
{
if (rejectHandlers == null)
{
rejectHandlers = new List();
}
rejectHandlers.Add(new RejectHandler { callback = onRejected, rejectable = rejectable });
}
///
/// Add a resolve handler for this promise.
///
private void AddResolveHandler(Action onResolved, IRejectable rejectable)
{
if (resolveCallbacks == null)
{
resolveCallbacks = new List>();
}
if (resolveRejectables == null)
{
resolveRejectables = new List();
}
resolveCallbacks.Add(onResolved);
resolveRejectables.Add(rejectable);
}
///
/// Add a progress handler for this promise.
///
private void AddProgressHandler(Action onProgress, IRejectable rejectable)
{
if (progressHandlers == null)
{
progressHandlers = new List();
}
progressHandlers.Add(new ProgressHandler { callback = onProgress, rejectable = rejectable });
}
///
/// Invoke a single handler.
///
private void InvokeHandler(Action callback, IRejectable rejectable, T value)
{
// Argument.NotNull(() => callback);
// Argument.NotNull(() => rejectable);
try
{
callback(value);
}
catch (Exception ex)
{
rejectable.Reject(ex);
}
}
///
/// Helper function clear out all handlers after resolution or rejection.
///
private void ClearHandlers()
{
rejectHandlers = null;
resolveCallbacks = null;
resolveRejectables = null;
progressHandlers = null;
}
///
/// Invoke all reject handlers.
///
private void InvokeRejectHandlers(Exception ex)
{
// Argument.NotNull(() => ex);
if (rejectHandlers != null)
{
rejectHandlers.Each(handler => InvokeHandler(handler.callback, handler.rejectable, ex));
}
ClearHandlers();
}
///
/// Invoke all resolve handlers.
///
private void InvokeResolveHandlers(PromisedT value)
{
if (resolveCallbacks != null)
{
for (int i = 0, maxI = resolveCallbacks.Count; i < maxI; i++) {
InvokeHandler(resolveCallbacks[i], resolveRejectables[i], value);
}
}
ClearHandlers();
}
///
/// Invoke all progress handlers.
///
private void InvokeProgressHandlers(float progress)
{
if (progressHandlers != null)
{
progressHandlers.Each(handler => InvokeHandler(handler.callback, handler.rejectable, progress));
}
}
///
/// Reject the promise with an exception.
///
public void Reject(Exception ex)
{
// Argument.NotNull(() => ex);
if (CurState != PromiseState.Pending)
{
throw new PromiseStateException(
"Attempt to reject a promise that is already in state: " + CurState
+ ", a promise can only be rejected when it is still in state: "
+ PromiseState.Pending
);
}
rejectionException = ex;
CurState = PromiseState.Rejected;
if (Promise.EnablePromiseTracking)
{
Promise.PendingPromises.Remove(this);
}
InvokeRejectHandlers(ex);
}
///
/// Resolve the promise with a particular value.
///
public void Resolve(PromisedT value)
{
if (CurState != PromiseState.Pending)
{
throw new PromiseStateException(
"Attempt to resolve a promise that is already in state: " + CurState
+ ", a promise can only be resolved when it is still in state: "
+ PromiseState.Pending
);
}
resolveValue = value;
CurState = PromiseState.Resolved;
if (Promise.EnablePromiseTracking)
{
Promise.PendingPromises.Remove(this);
}
InvokeResolveHandlers(value);
}
///
/// Report progress on the promise.
///
public void ReportProgress(float progress)
{
if (CurState != PromiseState.Pending)
{
throw new PromiseStateException(
"Attempt to report progress on a promise that is already in state: "
+ CurState + ", a promise can only report progress when it is still in state: "
+ PromiseState.Pending
);
}
InvokeProgressHandlers(progress);
}
///
/// Completes the promise.
/// onResolved is called on successful completion.
/// onRejected is called on error.
///
public void Done(Action onResolved, Action onRejected)
{
Then(onResolved, onRejected)
.Catch(ex =>
Promise.PropagateUnhandledException(this, ex)
);
}
///
/// Completes the promise.
/// onResolved is called on successful completion.
/// Adds a default error handler.
///
public void Done(Action onResolved)
{
Then(onResolved)
.Catch(ex =>
Promise.PropagateUnhandledException(this, ex)
);
}
///
/// Complete the promise. Adds a default error handler.
///
public void Done()
{
Catch(ex =>
Promise.PropagateUnhandledException(this, ex)
);
}
///
/// Set the name of the promise, useful for debugging.
///
public IPromise WithName(string name)
{
this.Name = name;
return this;
}
///
/// Handle errors for the promise.
///
public IPromise Catch(Action onRejected)
{
var resultPromise = new Promise();
resultPromise.WithName(Name);
Action resolveHandler = _ => resultPromise.Resolve();
Action rejectHandler = ex =>
{
try
{
onRejected(ex);
resultPromise.Resolve();
}
catch(Exception cbEx)
{
resultPromise.Reject(cbEx);
}
};
ActionHandlers(resultPromise, resolveHandler, rejectHandler);
ProgressHandlers(resultPromise, v => resultPromise.ReportProgress(v));
return resultPromise;
}
///
/// Handle errors for the promise.
///
public IPromise Catch(Func onRejected)
{
var resultPromise = new Promise();
resultPromise.WithName(Name);
Action resolveHandler = v => resultPromise.Resolve(v);
Action rejectHandler = ex =>
{
try
{
resultPromise.Resolve(onRejected(ex));
}
catch (Exception cbEx)
{
resultPromise.Reject(cbEx);
}
};
ActionHandlers(resultPromise, resolveHandler, rejectHandler);
ProgressHandlers(resultPromise, v => resultPromise.ReportProgress(v));
return resultPromise;
}
///
/// Add a resolved callback that chains a value promise (optionally converting to a different value type).
///
public IPromise Then(Func> onResolved)
{
return Then(onResolved, null, null);
}
///
/// Add a resolved callback that chains a non-value promise.
///
public IPromise Then(Func onResolved)
{
return Then(onResolved, null, null);
}
///
/// Add a resolved callback.
///
public IPromise Then(Action onResolved)
{
return Then(onResolved, null, null);
}
///
/// Add a resolved callback and a rejected callback.
/// The resolved callback chains a value promise (optionally converting to a different value type).
///
public IPromise Then(
Func> onResolved,
Func> onRejected
)
{
return Then(onResolved, onRejected, null);
}
///
/// Add a resolved callback and a rejected callback.
/// The resolved callback chains a non-value promise.
///
public IPromise Then(Func onResolved, Action onRejected)
{
return Then(onResolved, onRejected, null);
}
///
/// Add a resolved callback and a rejected callback.
///
public IPromise Then(Action onResolved, Action onRejected)
{
return Then(onResolved, onRejected, null);
}
///
/// Add a resolved callback, a rejected callback and a progress callback.
/// The resolved callback chains a value promise (optionally converting to a different value type).
///
public IPromise Then(
Func> onResolved,
Func> onRejected,
Action onProgress
)
{
// This version of the function must supply an onResolved.
// Otherwise there is now way to get the converted value to pass to the resulting promise.
// Argument.NotNull(() => onResolved);
var resultPromise = new Promise();
resultPromise.WithName(Name);
Action resolveHandler = v =>
{
onResolved(v)
.Progress(progress => resultPromise.ReportProgress(progress))
.Then(
// Should not be necessary to specify the arg type on the next line, but Unity (mono) has an internal compiler error otherwise.
chainedValue => resultPromise.Resolve(chainedValue),
ex => resultPromise.Reject(ex)
);
};
Action rejectHandler = ex =>
{
if (onRejected == null)
{
resultPromise.Reject(ex);
return;
}
try
{
onRejected(ex)
.Then(
chainedValue => resultPromise.Resolve(chainedValue),
callbackEx => resultPromise.Reject(callbackEx)
);
}
catch (Exception callbackEx)
{
resultPromise.Reject(callbackEx);
}
};
ActionHandlers(resultPromise, resolveHandler, rejectHandler);
if (onProgress != null)
{
ProgressHandlers(this, onProgress);
}
return resultPromise;
}
///
/// Add a resolved callback, a rejected callback and a progress callback.
/// The resolved callback chains a non-value promise.
///
public IPromise Then(Func onResolved, Action onRejected, Action onProgress)
{
var resultPromise = new Promise();
resultPromise.WithName(Name);
Action resolveHandler = v =>
{
if (onResolved != null)
{
onResolved(v)
.Progress(progress => resultPromise.ReportProgress(progress))
.Then(
() => resultPromise.Resolve(),
ex => resultPromise.Reject(ex)
);
}
else
{
resultPromise.Resolve();
}
};
Action rejectHandler = ex =>
{
if (onRejected != null)
{
onRejected(ex);
}
resultPromise.Reject(ex);
};
ActionHandlers(resultPromise, resolveHandler, rejectHandler);
if (onProgress != null)
{
ProgressHandlers(this, onProgress);
}
return resultPromise;
}
///
/// Add a resolved callback, a rejected callback and a progress callback.
///
public IPromise Then(Action onResolved, Action onRejected, Action onProgress)
{
var resultPromise = new Promise();
resultPromise.WithName(Name);
Action resolveHandler = v =>
{
if (onResolved != null)
{
onResolved(v);
}
resultPromise.Resolve();
};
Action rejectHandler = ex =>
{
if (onRejected != null)
{
onRejected(ex);
}
resultPromise.Reject(ex);
};
ActionHandlers(resultPromise, resolveHandler, rejectHandler);
if (onProgress != null)
{
ProgressHandlers(this, onProgress);
}
return resultPromise;
}
///
/// Return a new promise with a different value.
/// May also change the type of the value.
///
public IPromise Then(Func transform)
{
// Argument.NotNull(() => transform);
return Then(value => Promise.Resolved(transform(value)));
}
///
/// Helper function to invoke or register resolve/reject handlers.
///
private void ActionHandlers(IRejectable resultPromise, Action resolveHandler, Action rejectHandler)
{
if (CurState == PromiseState.Resolved)
{
Window.instance.scheduleMicrotask(() => {
InvokeHandler(resolveHandler, resultPromise, resolveValue);
});
}
else if (CurState == PromiseState.Rejected)
{
Window.instance.scheduleMicrotask(() => {
InvokeHandler(rejectHandler, resultPromise, rejectionException);
});
}
else
{
AddResolveHandler(resolveHandler, resultPromise);
AddRejectHandler(rejectHandler, resultPromise);
}
}
///
/// Helper function to invoke or register progress handlers.
///
private void ProgressHandlers(IRejectable resultPromise, Action progressHandler)
{
if (CurState == PromiseState.Pending)
{
AddProgressHandler(progressHandler, resultPromise);
}
}
///
/// Chain an enumerable of promises, all of which must resolve.
/// Returns a promise for a collection of the resolved results.
/// The resulting promise is resolved when all of the promises have resolved.
/// It is rejected as soon as any of the promises have been rejected.
///
public IPromise> ThenAll(Func>> chain)
{
return Then(value => Promise.All(chain(value)));
}
///
/// Chain an enumerable of promises, all of which must resolve.
/// Converts to a non-value promise.
/// The resulting promise is resolved when all of the promises have resolved.
/// It is rejected as soon as any of the promises have been rejected.
///
public IPromise ThenAll(Func> chain)
{
return Then(value => Promise.All(chain(value)));
}
///
/// Returns a promise that resolves when all of the promises in the enumerable argument have resolved.
/// Returns a promise of a collection of the resolved results.
///
public static IPromise> All(params IPromise[] promises)
{
return All((IEnumerable>)promises); // Cast is required to force use of the other All function.
}
///
/// Returns a promise that resolves when all of the promises in the enumerable argument have resolved.
/// Returns a promise of a collection of the resolved results.
///
public static IPromise> All(IEnumerable> promises)
{
var promisesArray = promises.ToArray();
if (promisesArray.Length == 0)
{
return Promise>.Resolved(Enumerable.Empty());
}
var remainingCount = promisesArray.Length;
var results = new PromisedT[remainingCount];
var progress = new float[remainingCount];
var resultPromise = new Promise>();
resultPromise.WithName("All");
promisesArray.Each((promise, index) =>
{
promise
.Progress(v =>
{
progress[index] = v;
if (resultPromise.CurState == PromiseState.Pending)
{
resultPromise.ReportProgress(progress.Average());
}
})
.Then(result =>
{
progress[index] = 1f;
results[index] = result;
--remainingCount;
if (remainingCount <= 0 && resultPromise.CurState == PromiseState.Pending)
{
// This will never happen if any of the promises errorred.
resultPromise.Resolve(results);
}
})
.Catch(ex =>
{
if (resultPromise.CurState == PromiseState.Pending)
{
// If a promise errorred and the result promise is still pending, reject it.
resultPromise.Reject(ex);
}
})
.Done();
});
return resultPromise;
}
///
/// Takes a function that yields an enumerable of promises.
/// Returns a promise that resolves when the first of the promises has resolved.
/// Yields the value from the first promise that has resolved.
///
public IPromise ThenRace(Func>> chain)
{
return Then(value => Promise.Race(chain(value)));
}
///
/// Takes a function that yields an enumerable of promises.
/// Converts to a non-value promise.
/// Returns a promise that resolves when the first of the promises has resolved.
/// Yields the value from the first promise that has resolved.
///
public IPromise ThenRace(Func> chain)
{
return Then(value => Promise.Race(chain(value)));
}
///
/// Returns a promise that resolves when the first of the promises in the enumerable argument have resolved.
/// Returns the value from the first promise that has resolved.
///
public static IPromise Race(params IPromise[] promises)
{
return Race((IEnumerable>)promises); // Cast is required to force use of the other function.
}
///
/// Returns a promise that resolves when the first of the promises in the enumerable argument have resolved.
/// Returns the value from the first promise that has resolved.
///
public static IPromise Race(IEnumerable> promises)
{
var promisesArray = promises.ToArray();
if (promisesArray.Length == 0)
{
throw new InvalidOperationException(
"At least 1 input promise must be provided for Race"
);
}
var resultPromise = new Promise();
resultPromise.WithName("Race");
var progress = new float[promisesArray.Length];
promisesArray.Each((promise, index) =>
{
promise
.Progress(v =>
{
if (resultPromise.CurState == PromiseState.Pending)
{
progress[index] = v;
resultPromise.ReportProgress(progress.Max());
}
})
.Then(result =>
{
if (resultPromise.CurState == PromiseState.Pending)
{
resultPromise.Resolve(result);
}
})
.Catch(ex =>
{
if (resultPromise.CurState == PromiseState.Pending)
{
// If a promise errorred and the result promise is still pending, reject it.
resultPromise.Reject(ex);
}
})
.Done();
});
return resultPromise;
}
///
/// Convert a simple value directly into a resolved promise.
///
public static IPromise Resolved(PromisedT promisedValue)
{
var promise = new Promise();
promise.Resolve(promisedValue);
return promise;
}
///
/// Convert an exception directly into a rejected promise.
///
public static IPromise Rejected(Exception ex)
{
// Argument.NotNull(() => ex);
var promise = new Promise();
promise.Reject(ex);
return promise;
}
public IPromise Finally(Action onComplete)
{
var promise = new Promise();
promise.WithName(Name);
this.Then(x => promise.Resolve(x));
this.Catch(e => {
try {
onComplete();
promise.Reject(e);
} catch (Exception ne) {
promise.Reject(ne);
}
});
return promise.Then(v =>
{
onComplete();
return v;
});
}
public IPromise ContinueWith(Func onComplete)
{
var promise = new Promise();
promise.WithName(Name);
this.Then(x => promise.Resolve());
this.Catch(e => promise.Resolve());
return promise.Then(onComplete);
}
public IPromise ContinueWith(Func> onComplete)
{
var promise = new Promise();
promise.WithName(Name);
this.Then(x => promise.Resolve());
this.Catch(e => promise.Resolve());
return promise.Then(onComplete);
}
public IPromise Progress(Action onProgress)
{
if (onProgress != null)
{
ProgressHandlers(this, onProgress);
}
return this;
}
}
}