From a2c3b74b3c4f51a5af42525860f06e19b7c4150e Mon Sep 17 00:00:00 2001 From: Harald Wolff Date: Sun, 24 Nov 2019 14:54:29 +0100 Subject: [PATCH] Promise implementation --- Promise.cs | 234 +++++++++++++++++++++++++++++++++++++++++++ ln.types.csproj | 2 + test/PromiseTests.cs | 113 +++++++++++++++++++++ 3 files changed, 349 insertions(+) create mode 100644 Promise.cs create mode 100644 test/PromiseTests.cs diff --git a/Promise.cs b/Promise.cs new file mode 100644 index 0000000..00506b9 --- /dev/null +++ b/Promise.cs @@ -0,0 +1,234 @@ +// /** +// * File: Promise.cs +// * Author: haraldwolff +// * +// * This file and it's content is copyrighted by the Author and / or copyright holder. +// * Any use wihtout proper permission is illegal and may lead to legal actions. +// * +// * +// **/ +using System; +using System.Threading; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +namespace ln.types +{ + public delegate void PromiseEvaluator(Action resolve, Action reject); + public delegate void PromiseEvaluator(I value, Action resolve, Action reject); + public delegate void PromiseResolved(T value); + public delegate void PromiseRejected(object error); + + public enum PromiseState { PENDING, RESOLVED, REJECTED } + + public class Promise + { + public event PromiseResolved OnResolve; + public event PromiseRejected OnReject; + + public PromiseState State { get; private set; } = PromiseState.PENDING; + + PromiseEvaluator Evaluator; + + T resolvedValue; + object reason; + + public T Value + { + get + { + if (State != PromiseState.RESOLVED) + throw new InvalidOperationException("promise not (yet) resolved"); + return resolvedValue; + } + } + public object Reason + { + get + { + if (State != PromiseState.REJECTED) + throw new InvalidOperationException("promise not (yet) rejected"); + return reason; + } + } + + private Promise() { } + + public Promise(PromiseEvaluator evaluator) : this(evaluator, true) { } + private Promise(PromiseEvaluator evaluator, bool queueJob) + { + Evaluator = evaluator; + if (queueJob) + ThreadPool.QueueUserWorkItem((state) => evaluator(resolve,reject )); + } + public Promise(Promise parent, Action fin) + { + OnResolve += (value) => fin(); + OnReject += (error) => fin(); + parent.OnResolve += (value) => this.Resolve(value); + parent.OnReject += (error) => this.Reject(error); + } + + protected Promise(PromiseResolved onResolve, PromiseRejected onReject) + { + OnResolve += onResolve; + OnReject += onReject; + } + protected Promise(PromiseRejected onReject) + { + OnReject += onReject; + } + + public Promise Then(PromiseResolved onResolve) => Then(onResolve, (e) => { }); + public Promise Then(PromiseResolved onResolve, PromiseRejected onRejected) => new ChainedPromise(this, (v, res, rej) => { res(v); onResolve(v); }, onRejected); + public Promise Finally(Action action) => new Promise(this, action); + + public Promise Then(PromiseEvaluator evaluator) => new ChainedPromise(this, evaluator, (e)=>{} ); + public Promise Then(PromiseEvaluator evaluator, PromiseRejected rejected) => new ChainedPromise(this, evaluator, rejected); + + + private void resolve(T value) + { + bool resolved; + lock (this) + { + resolved = (State == PromiseState.PENDING); + State = PromiseState.RESOLVED; + resolvedValue = value; + } + if (resolved) + OnResolve?.Invoke(value); + } + private void reject(object reason) + { + bool rejected; + lock (this) + { + rejected = (State == PromiseState.PENDING); + State = PromiseState.REJECTED; + this.reason = reason; + } + if (rejected) + OnReject?.Invoke(reason); + } + + public Promise Resolve(T value) + { + lock (this) + { + if (State != PromiseState.PENDING) + throw new InvalidOperationException("Promise already settled"); + + resolve(value); + } + return this; + } + public Promise Reject(object error) + { + lock (this) + { + if (State != PromiseState.PENDING) + throw new InvalidOperationException("Promise already settled"); + reject(error); + } + return this; + } + + public static Promise All(IEnumerable> promises) + { + Promise[] sources = promises.ToArray(); + T[] results = new T[sources.Length]; + int cresolved = 0; + + Promise promise = new Promise(); + for (int n = 0; n < sources.Length; n++) + { + Promise p = sources[n]; + int nn = n; + + lock (p) + { + switch (p.State) + { + case PromiseState.REJECTED: + return promise.Reject(p.Reason); + case PromiseState.RESOLVED: + cresolved++; + results[nn] = p.Value; + break; + case PromiseState.PENDING: + p.OnResolve += (value) => { + cresolved++; + results[nn] = p.Value; + + if (cresolved == results.Length) + promise.resolve(results); + }; + p.OnReject += (error) => { + promise.Reject(error); + }; + break; + } + } + } + if (cresolved == results.Length) + promise.resolve(results); + return promise; + } + + public static Promise Race(IEnumerable> promises) + { + Promise[] sources = promises.ToArray(); + Promise promise = new Promise(); + lock (promise) + { + for (int n = 0; n < sources.Length; n++) + { + Promise p = sources[n]; + int nn = n; + lock (p) + { + switch (p.State) + { + case PromiseState.REJECTED: + return promise.Reject(p.Reason); + case PromiseState.RESOLVED: + return promise.Resolve(p.Value); + case PromiseState.PENDING: + p.OnResolve += (value) => promise.resolve(value); + p.OnReject += (error) => promise.reject(error); + break; + } + } + } + } + return promise; + } + + class ChainedPromise : Promise + { + public ChainedPromise(Promise parent, PromiseEvaluator evalOnResolve, PromiseRejected onReject) + :base(onReject) + { + parent.OnResolve += (value) => { + evalOnResolve( + value, + (obj) => this.Resolve(obj), + (error) => this.Reject(error) + ); + }; + parent.OnReject += (error) => this.Reject(error); + } + } + + public static implicit operator Promise(T value) => new Promise().Resolve(value); + } + + public class Promise : Promise + { + public Promise(PromiseEvaluator evaluator) + : base(evaluator) + { + } + } +} diff --git a/ln.types.csproj b/ln.types.csproj index 4692b1e..6f39eae 100644 --- a/ln.types.csproj +++ b/ln.types.csproj @@ -129,6 +129,8 @@ + + diff --git a/test/PromiseTests.cs b/test/PromiseTests.cs new file mode 100644 index 0000000..4365a7a --- /dev/null +++ b/test/PromiseTests.cs @@ -0,0 +1,113 @@ +// /** +// * File: PromiseTests.cs +// * Author: haraldwolff +// * +// * This file and it's content is copyrighted by the Author and / or copyright holder. +// * Any use wihtout proper permission is illegal and may lead to legal actions. +// * +// * +// **/ +using NUnit.Framework; +using System; +using System.Threading; +namespace ln.types.test +{ + [TestFixture()] + public class PromiseTests + { + [Test()] + public void TestCase() + { + object l = new object(); + lock (l) + { + new Promise((resolve, reject) => + { + resolve(2); + }) + .Then( + (value, resolve, reject) => { + resolve(value); + }) + .Then((value) => + { + Console.WriteLine("Double Value: {0}",value); + }) + .Finally(() => { + lock (l) + { + Monitor.PulseAll(l); + } + }); + + + if (!Monitor.Wait(l, 500)) + throw new TimeoutException(); + } + } + + [Test()] + public void PromiseAll() + { + Promise[] promises = new Promise[5]; + for (int n = 0; n < promises.Length; n++) + { + promises[n] = new Promise((resolve, reject) => + { + ThreadPool.QueueUserWorkItem((state) => + { + Thread.Sleep(100 * n); + resolve(n); + }); + }); + } + + lock (promises) + { + Promise + .All(promises) + .Then((value) => + { + lock(promises) + Monitor.PulseAll(promises); + }); + + if (!Monitor.Wait(promises, 1500)) + throw new TimeoutException(); + } + } + + [Test()] + public void PromiseRace() + { + Promise[] promises = new Promise[5]; + for (int n = 0; n < promises.Length; n++) + { + promises[n] = new Promise((resolve, reject) => + { + ThreadPool.QueueUserWorkItem((state) => + { + Thread.Sleep(100 * n); + resolve(n); + }); + }); + } + + lock (promises) + { + Promise + .Race(promises) + .Then((value) => + { + lock (promises) + Monitor.PulseAll(promises); + }); + + if (!Monitor.Wait(promises, 1500)) + throw new TimeoutException(); + } + } + + + } +}