using System; using System.Collections.Generic; using System.Linq; using RSG.Exceptions; using RSG.Promises; using Unity.UIWidgets.ui; using UnityEngine; namespace RSG { /// /// Implements a non-generic C# promise, this is a promise that simply resolves without delivering a value. /// https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise /// public interface IPromise { /// /// ID of the promise, useful for debugging. /// 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); /// /// 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); /// /// Chain an enumerable of promises, all of which must resolve. /// 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); /// /// Chain a sequence of operations using promises. /// Reutrn a collection of functions each of which starts an async operation and yields a promise. /// Each function will be called and each promise resolved in turn. /// The resulting promise is resolved after each promise is resolved in sequence. /// IPromise ThenSequence(Func>> chain); /// /// Takes a function that yields an enumerable of promises. /// Returns a promise that resolves when the first of the promises has resolved. /// IPromise ThenRace(Func> chain); /// /// Takes a function that yields an enumerable of promises. /// Converts to a value promise. /// Returns a promise that resolves when the first of the promises 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 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(); /// /// Report progress in a promise. /// void ReportProgress(float progress); } /// /// Used to list information of pending promises. /// public interface IPromiseInfo { /// /// Id of the promise. /// int Id { get; } /// /// Human-readable name for the promise. /// string Name { get; } } /// /// Arguments to the UnhandledError event. /// public class ExceptionEventArgs : EventArgs { internal ExceptionEventArgs(Exception exception) { // Argument.NotNull(() => exception); this.Exception = exception; } public Exception Exception { get; private set; } } /// /// Represents a handler invoked when the promise is rejected. /// public struct RejectHandler { /// /// Callback fn. /// public Action callback; /// /// The promise that is rejected when there is an error while invoking the handler. /// public IRejectable rejectable; } public struct ProgressHandler { /// /// Callback fn. /// public Action callback; /// /// The promise that is rejected when there is an error while invoking the handler. /// public IRejectable rejectable; } /// /// Implements a non-generic C# promise, this is a promise that simply resolves without delivering a value. /// https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise /// public class Promise : IPromise, IPendingPromise, IPromiseInfo { /// /// Set to true to enable tracking of promises. /// public static bool EnablePromiseTracking = false; /// /// Event raised for unhandled errors. /// For this to work you have to complete your promises with a call to Done(). /// public static event EventHandler UnhandledException { add { unhandlerException += value; } remove { unhandlerException -= value; } } static EventHandler unhandlerException; /// /// Id for the next promise that is created. /// static int nextPromiseId; /// /// Information about pending promises. /// internal static readonly HashSet PendingPromises = new HashSet(); /// /// Information about pending promises, useful for debugging. /// This is only populated when 'EnablePromiseTracking' is set to true. /// public static IEnumerable GetPendingPromises() { return PendingPromises; } /// /// The exception when the promise is rejected. /// Exception rejectionException; /// /// Error handlers. /// List rejectHandlers; /// /// Represents a handler invoked when the promise is resolved. /// public struct ResolveHandler { /// /// Callback fn. /// public Action callback; /// /// The promise that is rejected when there is an error while invoking the handler. /// public IRejectable rejectable; } /// /// Completed handlers that accept no value. /// List resolveHandlers; /// /// Progress handlers. /// List progressHandlers; /// /// ID of the promise, useful for debugging. /// public int Id { get { return this.id; } } 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 bool IsSync { get; } public Promise(bool isSync = false) { this.IsSync = isSync; this.CurState = PromiseState.Pending; this.id = NextId(); if (EnablePromiseTracking) { PendingPromises.Add(this); } } public Promise(Action> resolver, bool isSync = false) { this.IsSync = isSync; this.CurState = PromiseState.Pending; this.id = NextId(); if (EnablePromiseTracking) { PendingPromises.Add(this); } try { resolver(this.Resolve, this.Reject); } catch (Exception ex) { this.Reject(ex); } } /// /// Increments the ID counter and gives us the ID for the next promise. /// internal static int NextId() { return ++nextPromiseId; } /// /// Add a rejection handler for this promise. /// void AddRejectHandler(Action onRejected, IRejectable rejectable) { if (this.rejectHandlers == null) { this.rejectHandlers = new List(); } this.rejectHandlers.Add(new RejectHandler { callback = onRejected, rejectable = rejectable }); } /// /// Add a resolve handler for this promise. /// void AddResolveHandler(Action onResolved, IRejectable rejectable) { if (this.resolveHandlers == null) { this.resolveHandlers = new List(); } this.resolveHandlers.Add(new ResolveHandler { callback = onResolved, rejectable = rejectable }); } /// /// Add a progress handler for this promise. /// void AddProgressHandler(Action onProgress, IRejectable rejectable) { if (this.progressHandlers == null) { this.progressHandlers = new List(); } this.progressHandlers.Add(new ProgressHandler {callback = onProgress, rejectable = rejectable}); } /// /// Invoke a single error handler. /// void InvokeRejectHandler(Action callback, IRejectable rejectable, Exception value) { // Argument.NotNull(() => callback); // Argument.NotNull(() => rejectable); try { callback(value); } catch (Exception ex) { rejectable.Reject(ex); } } /// /// Invoke a single resolve handler. /// void InvokeResolveHandler(Action callback, IRejectable rejectable) { // Argument.NotNull(() => callback); // Argument.NotNull(() => rejectable); try { callback(); } catch (Exception ex) { rejectable.Reject(ex); } } /// /// Invoke a single progress handler. /// void InvokeProgressHandler(Action callback, IRejectable rejectable, float progress) { // Argument.NotNull(() => callback); // Argument.NotNull(() => rejectable); try { callback(progress); } catch (Exception ex) { rejectable.Reject(ex); } } /// /// Helper function clear out all handlers after resolution or rejection. /// void ClearHandlers() { this.rejectHandlers = null; this.resolveHandlers = null; this.progressHandlers = null; } /// /// Invoke all reject handlers. /// void InvokeRejectHandlers(Exception ex) { // Argument.NotNull(() => ex); if (this.rejectHandlers != null) { this.rejectHandlers.Each(handler => this.InvokeRejectHandler(handler.callback, handler.rejectable, ex)); } else { PropagateUnhandledException(this, ex); } this.ClearHandlers(); } /// /// Invoke all resolve handlers. /// void InvokeResolveHandlers() { if (this.resolveHandlers != null) { this.resolveHandlers.Each(handler => this.InvokeResolveHandler(handler.callback, handler.rejectable)); } this.ClearHandlers(); } /// /// Invoke all progress handlers. /// void InvokeProgressHandlers(float progress) { if (this.progressHandlers != null) { this.progressHandlers.Each(handler => this.InvokeProgressHandler(handler.callback, handler.rejectable, progress)); } } /// /// Reject the promise with an exception. /// public void Reject(Exception ex) { if (this.IsSync) { this.RejectSync(ex); } else { Window.instance.run(() => this.RejectSync(ex)); } } public void RejectSync(Exception ex) { // Argument.NotNull(() => ex); if (this.CurState != PromiseState.Pending) { throw new PromiseStateException( "Attempt to reject a promise that is already in state: " + this.CurState + ", a promise can only be rejected when it is still in state: " + PromiseState.Pending ); } this.rejectionException = ex; this.CurState = PromiseState.Rejected; if (EnablePromiseTracking) { PendingPromises.Remove(this); } this.InvokeRejectHandlers(ex); } /// /// Resolve the promise with a particular value. /// public void Resolve() { if (this.IsSync) { this.ResolveSync(); } else { Window.instance.run(() => this.ResolveSync()); } } public void ResolveSync() { if (this.CurState != PromiseState.Pending) { throw new PromiseStateException( "Attempt to resolve a promise that is already in state: " + this.CurState + ", a promise can only be resolved when it is still in state: " + PromiseState.Pending ); } this.CurState = PromiseState.Resolved; if (EnablePromiseTracking) { PendingPromises.Remove(this); } this.InvokeResolveHandlers(); } /// /// Report progress on the promise. /// public void ReportProgress(float progress) { if (this.CurState != PromiseState.Pending) { throw new PromiseStateException( "Attempt to report progress on a promise that is already in state: " + this.CurState + ", a promise can only report progress when it is still in state: " + PromiseState.Pending ); } this.InvokeProgressHandlers(progress); } /// /// Completes the promise. /// onResolved is called on successful completion. /// onRejected is called on error. /// public void Done(Action onResolved, Action onRejected) { this.Then(onResolved, onRejected) .Catch(ex => PropagateUnhandledException(this, ex) ); } /// /// Completes the promise. /// onResolved is called on successful completion. /// Adds a default error handler. /// public void Done(Action onResolved) { this.Then(onResolved) .Catch(ex => PropagateUnhandledException(this, ex) ); } /// /// Complete the promise. Adds a defualt error handler. /// public void Done() { this.Catch(ex => 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) { // Argument.NotNull(() => onRejected); var resultPromise = new Promise(isSync: true); resultPromise.WithName(this.Name); Action resolveHandler = () => resultPromise.Resolve(); Action rejectHandler = ex => { try { onRejected(ex); resultPromise.Resolve(); } catch (Exception callbackException) { resultPromise.Reject(callbackException); } }; this.ActionHandlers(resultPromise, resolveHandler, rejectHandler); this.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 this.Then(onResolved, null, null); } /// /// Add a resolved callback that chains a non-value promise. /// public IPromise Then(Func onResolved) { return this.Then(onResolved, null, null); } /// /// Add a resolved callback. /// public IPromise Then(Action onResolved) { return this.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 this.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 this.Then(onResolved, onRejected, null); } /// /// Add a resolved callback and a rejected callback. /// public IPromise Then(Action onResolved, Action onRejected) { return this.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(this.Name); Action resolveHandler = () => { onResolved() .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); } }; this.ActionHandlers(resultPromise, resolveHandler, rejectHandler); if (onProgress != null) { this.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(isSync: true); resultPromise.WithName(this.Name); Action resolveHandler = () => { if (onResolved != null) { onResolved() .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); }; this.ActionHandlers(resultPromise, resolveHandler, rejectHandler); if (onProgress != null) { this.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(isSync: true); resultPromise.WithName(this.Name); Action resolveHandler = () => { if (onResolved != null) { onResolved(); } resultPromise.Resolve(); }; Action rejectHandler = ex => { if (onRejected != null) { onRejected(ex); resultPromise.Resolve(); return; } resultPromise.Reject(ex); }; this.ActionHandlers(resultPromise, resolveHandler, rejectHandler); if (onProgress != null) { this.ProgressHandlers(this, onProgress); } return resultPromise; } /// /// Helper function to invoke or register resolve/reject handlers. /// void ActionHandlers(IRejectable resultPromise, Action resolveHandler, Action rejectHandler) { if (this.CurState == PromiseState.Resolved) { this.InvokeResolveHandler(resolveHandler, resultPromise); } else if (this.CurState == PromiseState.Rejected) { this.InvokeRejectHandler(rejectHandler, resultPromise, this.rejectionException); } else { this.AddResolveHandler(resolveHandler, resultPromise); this.AddRejectHandler(rejectHandler, resultPromise); } } /// /// Helper function to invoke or register progress handlers. /// void ProgressHandlers(IRejectable resultPromise, Action progressHandler) { if (this.CurState == PromiseState.Pending) { this.AddProgressHandler(progressHandler, resultPromise); } } /// /// Chain an enumerable of promises, all of which must resolve. /// 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 this.Then(() => All(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. /// public IPromise> ThenAll(Func>> chain) { return this.Then(() => Promise.All(chain())); } /// /// 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 Resolved(); } var remainingCount = promisesArray.Length; var resultPromise = new Promise(isSync: true); resultPromise.WithName("All"); var progress = new float[remainingCount]; promisesArray.Each((promise, index) => { promise .Progress(v => { progress[index] = v; if (resultPromise.CurState == PromiseState.Pending) { resultPromise.ReportProgress(progress.Average()); } }) .Then(() => { progress[index] = 1f; --remainingCount; if (remainingCount <= 0 && resultPromise.CurState == PromiseState.Pending) { // This will never happen if any of the promises errorred. resultPromise.Resolve(); } }) .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; } /// /// Chain a sequence of operations using promises. /// Reutrn a collection of functions each of which starts an async operation and yields a promise. /// Each function will be called and each promise resolved in turn. /// The resulting promise is resolved after each promise is resolved in sequence. /// public IPromise ThenSequence(Func>> chain) { return this.Then(() => Sequence(chain())); } /// /// Chain a number of operations using promises. /// Takes a number of functions each of which starts an async operation and yields a promise. /// public static IPromise Sequence(params Func[] fns) { return Sequence((IEnumerable>) fns); } /// /// Chain a sequence of operations using promises. /// Takes a collection of functions each of which starts an async operation and yields a promise. /// public static IPromise Sequence(IEnumerable> fns) { var promise = new Promise(isSync: true); int count = 0; fns.Aggregate( Resolved(), (prevPromise, fn) => { int itemSequence = count; ++count; return prevPromise .Then(() => { var sliceLength = 1f / count; promise.ReportProgress(sliceLength * itemSequence); return fn(); }) .Progress(v => { var sliceLength = 1f / count; promise.ReportProgress(sliceLength * (v + itemSequence)); }) ; } ) .Then(() => promise.Resolve()) .Catch(promise.Reject); return promise; } /// /// Takes a function that yields an enumerable of promises. /// Returns a promise that resolves when the first of the promises has resolved. /// public IPromise ThenRace(Func> chain) { return this.Then(() => Race(chain())); } /// /// Takes a function that yields an enumerable of promises. /// Converts to a value promise. /// Returns a promise that resolves when the first of the promises has resolved. /// public IPromise ThenRace(Func>> chain) { return this.Then(() => Promise.Race(chain())); } /// /// 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(isSync: true); resultPromise.WithName("Race"); var progress = new float[promisesArray.Length]; promisesArray.Each((promise, index) => { promise .Progress(v => { progress[index] = v; resultPromise.ReportProgress(progress.Max()); }) .Catch(ex => { if (resultPromise.CurState == PromiseState.Pending) { // If a promise errorred and the result promise is still pending, reject it. resultPromise.Reject(ex); } }) .Then(() => { if (resultPromise.CurState == PromiseState.Pending) { resultPromise.Resolve(); } }) .Done(); }); return resultPromise; } /// /// Convert a simple value directly into a resolved promise. /// public static IPromise Resolved() { var promise = new Promise(isSync: true); promise.Resolve(); 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(isSync: true); promise.WithName(this.Name); this.Then(() => promise.Resolve()); this.Catch(e => { try { onComplete(); promise.Reject(e); } catch (Exception ne) { promise.Reject(ne); } }); return promise.Then(onComplete); } public IPromise ContinueWith(Func onComplete) { var promise = new Promise(isSync: true); promise.WithName(this.Name); this.Then(() => promise.Resolve()); this.Catch(e => promise.Resolve()); return promise.Then(onComplete); } public IPromise ContinueWith(Func> onComplete) { var promise = new Promise(isSync: true); promise.WithName(this.Name); this.Then(() => promise.Resolve()); this.Catch(e => promise.Resolve()); return promise.Then(onComplete); } public IPromise Progress(Action onProgress) { if (onProgress != null) { this.ProgressHandlers(this, onProgress); } return this; } /// /// Raises the UnhandledException event. /// internal static void PropagateUnhandledException(object sender, Exception ex) { if (unhandlerException != null) { unhandlerException(sender, new ExceptionEventArgs(ex)); } else { Debug.LogWarning("Unhandled Exception from " + sender + ": " + ex); } } } }