using ZenFulcrum.EmbeddedBrowser.Promises; using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Text; namespace ZenFulcrum.EmbeddedBrowser { /// <summary> /// Implements a C# promise. /// https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise /// /// This can also be waited on in a Unity coroutine and queried for its value. /// </summary> public interface IPromise<PromisedT> { /// <summary> /// Set the name of the promise, useful for debugging. /// </summary> IPromise<PromisedT> WithName(string name); /// <summary> /// Completes the promise. /// onResolved is called on successful completion. /// onRejected is called on error. /// </summary> void Done(Action<PromisedT> onResolved, Action<Exception> onRejected); /// <summary> /// Completes the promise. /// onResolved is called on successful completion. /// Adds a default error handler. /// </summary> void Done(Action<PromisedT> onResolved); /// <summary> /// Complete the promise. Adds a default error handler. /// </summary> void Done(); /// <summary> /// Handle errors for the promise. /// </summary> IPromise<PromisedT> Catch(Action<Exception> onRejected); /// <summary> /// Add a resolved callback that chains a value promise (optionally converting to a different value type). /// </summary> IPromise<ConvertedT> Then<ConvertedT>(Func<PromisedT, IPromise<ConvertedT>> onResolved); /// <summary> /// Add a resolved callback that chains a non-value promise. /// </summary> IPromise Then(Func<PromisedT, IPromise> onResolved); /// <summary> /// Add a resolved callback. /// </summary> IPromise<PromisedT> Then(Action<PromisedT> onResolved); /// <summary> /// Add a resolved callback and a rejected callback. /// The resolved callback chains a value promise (optionally converting to a different value type). /// </summary> IPromise<ConvertedT> Then<ConvertedT>(Func<PromisedT, IPromise<ConvertedT>> onResolved, Action<Exception> onRejected); /// <summary> /// Add a resolved callback and a rejected callback. /// The resolved callback chains a non-value promise. /// </summary> IPromise Then(Func<PromisedT, IPromise> onResolved, Action<Exception> onRejected); /// <summary> /// Add a resolved callback and a rejected callback. /// </summary> IPromise<PromisedT> Then(Action<PromisedT> onResolved, Action<Exception> onRejected); /// <summary> /// Return a new promise with a different value. /// May also change the type of the value. /// </summary> IPromise<ConvertedT> Then<ConvertedT>(Func<PromisedT, ConvertedT> transform); /// <summary> /// Return a new promise with a different value. /// May also change the type of the value. /// </summary> [Obsolete("Use Then instead")] IPromise<ConvertedT> Transform<ConvertedT>(Func<PromisedT, ConvertedT> transform); /// <summary> /// 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. /// </summary> IPromise<IEnumerable<ConvertedT>> ThenAll<ConvertedT>(Func<PromisedT, IEnumerable<IPromise<ConvertedT>>> chain); /// <summary> /// 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. /// </summary> IPromise ThenAll(Func<PromisedT, IEnumerable<IPromise>> chain); /// <summary> /// 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. /// </summary> IPromise<ConvertedT> ThenRace<ConvertedT>(Func<PromisedT, IEnumerable<IPromise<ConvertedT>>> chain); /// <summary> /// 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. /// </summary> IPromise ThenRace(Func<PromisedT, IEnumerable<IPromise>> chain); /// <summary> /// Returns the resulting value if resolved. /// Throws the rejection if rejected. /// Throws an exception if not settled. /// </summary> PromisedT Value { get; } /// <summary> /// Returns an enumerable that yields null until the promise is settled. /// ("To WaitFor" like the WaitForXXYY functions Unity provides.) /// Suitable for use with a Unity coroutine's "yield return promise.ToWaitFor()" /// Once it finishes, use promise.Value to retrieve the value/error. /// /// If throwOnFail is true, the coroutine will abort on promise rejection. /// </summary> /// <returns></returns> IEnumerator ToWaitFor(bool abortOnFail = false); } /// <summary> /// Interface for a promise that can be rejected. /// </summary> public interface IRejectable { /// <summary> /// Reject the promise with an exception. /// </summary> void Reject(Exception ex); } /// <summary> /// Interface for a promise that can be rejected or resolved. /// </summary> public interface IPendingPromise<PromisedT> : IRejectable { /// <summary> /// Resolve the promise with a particular value. /// </summary> void Resolve(PromisedT value); } /// <summary> /// Specifies the state of a promise. /// </summary> public enum PromiseState { Pending, // The promise is in-flight. Rejected, // The promise has been rejected. Resolved // The promise has been resolved. }; /// <summary> /// Implements a C# promise. /// https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise /// </summary> public class Promise<PromisedT> : IPromise<PromisedT>, IPendingPromise<PromisedT>, IPromiseInfo { /// <summary> /// The exception when the promise is rejected. /// </summary> private Exception rejectionException; /// <summary> /// The value when the promises is resolved. /// </summary> private PromisedT resolveValue; /// <summary> /// Error handler. /// </summary> private List<RejectHandler> rejectHandlers; /// <summary> /// Completed handlers that accept a value. /// </summary> private List<Action<PromisedT>> resolveCallbacks; private List<IRejectable> resolveRejectables; /// <summary> /// ID of the promise, useful for debugging. /// </summary> public int Id { get; private set; } /// <summary> /// Name of the promise, when set, useful for debugging. /// </summary> public string Name { get; private set; } /// <summary> /// Tracks the current state of the promise. /// </summary> public PromiseState CurState { get; private set; } public Promise() { this.CurState = PromiseState.Pending; this.Id = ++Promise.nextPromiseId; if (Promise.EnablePromiseTracking) { Promise.pendingPromises.Add(this); } } public Promise(Action<Action<PromisedT>, Action<Exception>> resolver) { this.CurState = PromiseState.Pending; this.Id = ++Promise.nextPromiseId; if (Promise.EnablePromiseTracking) { Promise.pendingPromises.Add(this); } try { resolver( // Resolve value => Resolve(value), // Reject ex => Reject(ex) ); } catch (Exception ex) { Reject(ex); } } /// <summary> /// Add a rejection handler for this promise. /// </summary> private void AddRejectHandler(Action<Exception> onRejected, IRejectable rejectable) { if (rejectHandlers == null) { rejectHandlers = new List<RejectHandler>(); } rejectHandlers.Add(new RejectHandler() { callback = onRejected, rejectable = rejectable }); ; } /// <summary> /// Add a resolve handler for this promise. /// </summary> private void AddResolveHandler(Action<PromisedT> onResolved, IRejectable rejectable) { if (resolveCallbacks == null) { resolveCallbacks = new List<Action<PromisedT>>(); } if (resolveRejectables == null) { resolveRejectables = new List<IRejectable>(); } resolveCallbacks.Add(onResolved); resolveRejectables.Add(rejectable); } /// <summary> /// Invoke a single handler. /// </summary> private void InvokeHandler<T>(Action<T> callback, IRejectable rejectable, T value) { // Argument.NotNull(() => callback); // Argument.NotNull(() => rejectable); try { callback(value); } catch (Exception ex) { rejectable.Reject(ex); } } /// <summary> /// Helper function clear out all handlers after resolution or rejection. /// </summary> private void ClearHandlers() { rejectHandlers = null; resolveCallbacks = null; resolveRejectables = null; } /// <summary> /// Invoke all reject handlers. /// </summary> private void InvokeRejectHandlers(Exception ex) { // Argument.NotNull(() => ex); if (rejectHandlers != null) { rejectHandlers.Each(handler => InvokeHandler(handler.callback, handler.rejectable, ex)); } ClearHandlers(); } /// <summary> /// Invoke all resolve handlers. /// </summary> 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(); } /// <summary> /// Reject the promise with an exception. /// </summary> public void Reject(Exception ex) { // Argument.NotNull(() => ex); if (CurState != PromiseState.Pending) { throw new ApplicationException("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); } /// <summary> /// Resolve the promise with a particular value. /// </summary> public void Resolve(PromisedT value) { if (CurState != PromiseState.Pending) { throw new ApplicationException("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); } /// <summary> /// Completes the promise. /// onResolved is called on successful completion. /// onRejected is called on error. /// </summary> public void Done(Action<PromisedT> onResolved, Action<Exception> onRejected) { Then(onResolved, onRejected) .Catch(ex => Promise.PropagateUnhandledException(this, ex) ); } /// <summary> /// Completes the promise. /// onResolved is called on successful completion. /// Adds a default error handler. /// </summary> public void Done(Action<PromisedT> onResolved) { Then(onResolved) .Catch(ex => Promise.PropagateUnhandledException(this, ex) ); } /// <summary> /// Complete the promise. Adds a default error handler. /// </summary> public void Done() { Catch(ex => Promise.PropagateUnhandledException(this, ex) ); } /// <summary> /// Set the name of the promise, useful for debugging. /// </summary> public IPromise<PromisedT> WithName(string name) { this.Name = name; return this; } /// <summary> /// Handle errors for the promise. /// </summary> public IPromise<PromisedT> Catch(Action<Exception> onRejected) { // Argument.NotNull(() => onRejected); var resultPromise = new Promise<PromisedT>(); resultPromise.WithName(Name); Action<PromisedT> resolveHandler = v => { resultPromise.Resolve(v); }; Action<Exception> rejectHandler = ex => { onRejected(ex); resultPromise.Reject(ex); }; ActionHandlers(resultPromise, resolveHandler, rejectHandler); return resultPromise; } /// <summary> /// Add a resolved callback that chains a value promise (optionally converting to a different value type). /// </summary> public IPromise<ConvertedT> Then<ConvertedT>(Func<PromisedT, IPromise<ConvertedT>> onResolved) { return Then(onResolved, null); } /// <summary> /// Add a resolved callback that chains a non-value promise. /// </summary> public IPromise Then(Func<PromisedT, IPromise> onResolved) { return Then(onResolved, null); } /// <summary> /// Add a resolved callback. /// </summary> public IPromise<PromisedT> Then(Action<PromisedT> onResolved) { return Then(onResolved, null); } /// <summary> /// Add a resolved callback and a rejected callback. /// The resolved callback chains a value promise (optionally converting to a different value type). /// </summary> public IPromise<ConvertedT> Then<ConvertedT>(Func<PromisedT, IPromise<ConvertedT>> onResolved, Action<Exception> onRejected) { // 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<ConvertedT>(); resultPromise.WithName(Name); Action<PromisedT> resolveHandler = v => { onResolved(v) .Then( // Should not be necessary to specify the arg type on the next line, but Unity (mono) has an internal compiler error otherwise. (ConvertedT chainedValue) => resultPromise.Resolve(chainedValue), ex => resultPromise.Reject(ex) ); }; Action<Exception> rejectHandler = ex => { if (onRejected != null) { onRejected(ex); } resultPromise.Reject(ex); }; ActionHandlers(resultPromise, resolveHandler, rejectHandler); return resultPromise; } /// <summary> /// Add a resolved callback and a rejected callback. /// The resolved callback chains a non-value promise. /// </summary> public IPromise Then(Func<PromisedT, IPromise> onResolved, Action<Exception> onRejected) { var resultPromise = new Promise(); resultPromise.WithName(Name); Action<PromisedT> resolveHandler = v => { if (onResolved != null) { onResolved(v) .Then( () => resultPromise.Resolve(), ex => resultPromise.Reject(ex) ); } else { resultPromise.Resolve(); } }; Action<Exception> rejectHandler = ex => { if (onRejected != null) { onRejected(ex); } resultPromise.Reject(ex); }; ActionHandlers(resultPromise, resolveHandler, rejectHandler); return resultPromise; } /// <summary> /// Add a resolved callback and a rejected callback. /// </summary> public IPromise<PromisedT> Then(Action<PromisedT> onResolved, Action<Exception> onRejected) { var resultPromise = new Promise<PromisedT>(); resultPromise.WithName(Name); Action<PromisedT> resolveHandler = v => { if (onResolved != null) { onResolved(v); } resultPromise.Resolve(v); }; Action<Exception> rejectHandler = ex => { if (onRejected != null) { onRejected(ex); } resultPromise.Reject(ex); }; ActionHandlers(resultPromise, resolveHandler, rejectHandler); return resultPromise; } /// <summary> /// Return a new promise with a different value. /// May also change the type of the value. /// </summary> public IPromise<ConvertedT> Then<ConvertedT>(Func<PromisedT, ConvertedT> transform) { // Argument.NotNull(() => transform); return Then(value => Promise<ConvertedT>.Resolved(transform(value))); } /// <summary> /// Return a new promise with a different value. /// May also change the type of the value. /// </summary> [Obsolete("Use Then instead")] public IPromise<ConvertedT> Transform<ConvertedT>(Func<PromisedT, ConvertedT> transform) { // Argument.NotNull(() => transform); return Then(value => Promise<ConvertedT>.Resolved(transform(value))); } /// <summary> /// Helper function to invoke or register resolve/reject handlers. /// </summary> private void ActionHandlers(IRejectable resultPromise, Action<PromisedT> resolveHandler, Action<Exception> rejectHandler) { if (CurState == PromiseState.Resolved) { InvokeHandler(resolveHandler, resultPromise, resolveValue); } else if (CurState == PromiseState.Rejected) { InvokeHandler(rejectHandler, resultPromise, rejectionException); } else { AddResolveHandler(resolveHandler, resultPromise); AddRejectHandler(rejectHandler, resultPromise); } } /// <summary> /// 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. /// </summary> public IPromise<IEnumerable<ConvertedT>> ThenAll<ConvertedT>(Func<PromisedT, IEnumerable<IPromise<ConvertedT>>> chain) { return Then(value => Promise<ConvertedT>.All(chain(value))); } /// <summary> /// 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. /// </summary> public IPromise ThenAll(Func<PromisedT, IEnumerable<IPromise>> chain) { return Then(value => Promise.All(chain(value))); } /// <summary> /// 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. /// </summary> public static IPromise<IEnumerable<PromisedT>> All(params IPromise<PromisedT>[] promises) { return All((IEnumerable<IPromise<PromisedT>>)promises); // Cast is required to force use of the other All function. } /// <summary> /// 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. /// </summary> public static IPromise<IEnumerable<PromisedT>> All(IEnumerable<IPromise<PromisedT>> promises) { var promisesArray = promises.ToArray(); if (promisesArray.Length == 0) { return Promise<IEnumerable<PromisedT>>.Resolved(EnumerableExt.Empty<PromisedT>()); } var remainingCount = promisesArray.Length; var results = new PromisedT[remainingCount]; var resultPromise = new Promise<IEnumerable<PromisedT>>(); resultPromise.WithName("All"); promisesArray.Each((promise, index) => { promise .Catch(ex => { if (resultPromise.CurState == PromiseState.Pending) { // If a promise errorred and the result promise is still pending, reject it. resultPromise.Reject(ex); } }) .Then(result => { results[index] = result; --remainingCount; if (remainingCount <= 0) { // This will never happen if any of the promises errorred. resultPromise.Resolve(results); } }) .Done(); }); return resultPromise; } /// <summary> /// 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. /// </summary> public IPromise<ConvertedT> ThenRace<ConvertedT>(Func<PromisedT, IEnumerable<IPromise<ConvertedT>>> chain) { return Then(value => Promise<ConvertedT>.Race(chain(value))); } /// <summary> /// 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. /// </summary> public IPromise ThenRace(Func<PromisedT, IEnumerable<IPromise>> chain) { return Then(value => Promise.Race(chain(value))); } public PromisedT Value { get { if (CurState == PromiseState.Pending) throw new InvalidOperationException("Promise not settled"); else if (CurState == PromiseState.Rejected) throw rejectionException; return resolveValue; } } class Enumerated<T> : IEnumerator { private Promise<T> promise; private bool abortOnFail; public Enumerated(Promise<T> promise, bool abortOnFail) { this.promise = promise; this.abortOnFail = abortOnFail; } public bool MoveNext() { if (abortOnFail && promise.CurState == PromiseState.Rejected) { throw promise.rejectionException; } return promise.CurState == PromiseState.Pending; } public void Reset() { } public object Current { get { return null; } } } public IEnumerator ToWaitFor(bool abortOnFail) { var ret = new Enumerated<PromisedT>(this, abortOnFail); //someone will poll for completion, so act like we've been terminated Done(x => {}, ex => {}); return ret; } /// <summary> /// 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. /// </summary> public static IPromise<PromisedT> Race(params IPromise<PromisedT>[] promises) { return Race((IEnumerable<IPromise<PromisedT>>)promises); // Cast is required to force use of the other function. } /// <summary> /// 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. /// </summary> public static IPromise<PromisedT> Race(IEnumerable<IPromise<PromisedT>> promises) { var promisesArray = promises.ToArray(); if (promisesArray.Length == 0) { throw new ApplicationException("At least 1 input promise must be provided for Race"); } var resultPromise = new Promise<PromisedT>(); resultPromise.WithName("Race"); promisesArray.Each((promise, index) => { promise .Catch(ex => { if (resultPromise.CurState == PromiseState.Pending) { // If a promise errorred and the result promise is still pending, reject it. resultPromise.Reject(ex); } }) .Then(result => { if (resultPromise.CurState == PromiseState.Pending) { resultPromise.Resolve(result); } }) .Done(); }); return resultPromise; } /// <summary> /// Convert a simple value directly into a resolved promise. /// </summary> public static IPromise<PromisedT> Resolved(PromisedT promisedValue) { var promise = new Promise<PromisedT>(); promise.Resolve(promisedValue); return promise; } /// <summary> /// Convert an exception directly into a rejected promise. /// </summary> public static IPromise<PromisedT> Rejected(Exception ex) { // Argument.NotNull(() => ex); var promise = new Promise<PromisedT>(); promise.Reject(ex); return promise; } } }