rd-net/Lifetimes/Collections/Viewable/ReactiveEx.cs (339 lines of code) (raw):
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using JetBrains.Core;
using JetBrains.Lifetimes;
// ReSharper disable InconsistentNaming
namespace JetBrains.Collections.Viewable
{
public static class ReactiveEx
{
public static void Fire(this ISignal<Unit> me)
{
me.Fire(Unit.Instance);
}
public static void AddLifetimed<T>(this ICollection<T> me, Lifetime lifetime, T item)
{
lifetime.Bracket(() => me.Add(item), () => me.Remove(item));
}
public static void Advise(this ISource<Unit> me, Lifetime lifetime, Action handler)
{
me.Advise(lifetime, _ => { handler(); });
}
public static void AdviseNotNull<T>(this ISource<T?> me, Lifetime lifetime, Action<T> handler) where T : class
{
me.Advise(lifetime, v => { if (v != null) handler(v); });
}
public static void AdviseNotNull<T>(this ISource<T?> me, Lifetime lifetime, Action<T> handler) where T : struct
{
me.Advise(lifetime, v => { if (v != null) handler(v.Value); });
}
public static void AdviseUntil<T>(this ISource<T> me, Lifetime lifetime, Func<T, bool> handler)
{
if (!lifetime.IsAlive) return;
var definition = Lifetime.Define(lifetime);
me.Advise(definition.Lifetime, v =>
{
if (handler(v)) definition.Terminate();
});
}
public static void AdviseOnce<T>(this ISource<T> me, Lifetime lifetime, Action<T> handler)
{
if (lifetime.IsNotAlive) return;
var definition = lifetime.CreateNested();
me.Advise(definition.Lifetime, v =>
{
definition.Terminate();
handler(v);
});
}
public static void View<T>(this IReadonlyProperty<T> me, Lifetime lifetime, Action<Lifetime, T> handler)
{
if (!lifetime.IsAlive) return;
// nested lifetime is needed due to exception that could be thrown
// while viewing a property change right at the moment of <param>lifetime</param>'s termination
// but before <param>handler</param> gets removed
var lf = lifetime == Lifetime.Eternal ? lifetime : Lifetime.Define(lifetime).Lifetime;
var seq = new SequentialLifetimes(lf);
me.Advise(lf, v => handler(seq.Next(), v));
}
public static void ViewNotNull<T>(this IReadonlyProperty<T> me, Lifetime lifetime, Action<Lifetime, T> handler) where T: class
{
me.View(lifetime, (lf, v) =>
{
if (v != null) handler(lf, v);
});
}
public static void ViewNotNull<T>(this IReadonlyProperty<T?> me, Lifetime lifetime, Action<Lifetime, T> handler) where T: struct
{
me.View(lifetime, (lf, v) =>
{
if (v != null) handler(lf, v.Value);
});
}
public static void ViewNull<T>(this IReadonlyProperty<T> me, Lifetime lifetime, Action<Lifetime> handler) where T: class
{
me.View(lifetime, (lf, v) =>
{
if (v == null) handler(lf);
});
}
public static void ViewNull<T>(this IReadonlyProperty<T?> me, Lifetime lifetime, Action<Lifetime> handler) where T: struct
{
me.View(lifetime, (lf, v) =>
{
if (v == null) handler(lf);
});
}
public static void View<K, V>(this IViewableMap<K, V> me, Lifetime lifetime, Action<Lifetime, KeyValuePair<K, V>> handler) where K: notnull
{
View(me, lifetime, (lf, k, v) => handler(lf, JetKeyValuePair.Of(k, v)));
}
public static void FlowInto<K, V>(this IViewableMap<K, V> me, Lifetime lifetime, IDictionary<K, V> storage) where K : notnull
{
me.Advise(lifetime, e =>
{
if (e.IsAdd || e.IsUpdate)
storage[e.Key] = e.NewValue;
else if (e.IsRemove)
storage.Remove(e.Key);
else
throw new ArgumentOutOfRangeException($"Unexpected kind: {e.Kind}");
});
}
[Obsolete("This method has horrible performance when adding 100+ items")]
public static void AddOrReplaceLifetimed<K, V>(this IViewableMap<K, V> me, Lifetime lifetime, K k, Func<Lifetime, V> vfun) where K : notnull
{
var def = lifetime.CreateNested();
me[k] = vfun(def.Lifetime);
me.Change.Advise(def.Lifetime, _event =>
{
if (_event.Key.Equals(k)) def.Terminate();
});
}
public static void AdviseAddRemove<K, V>(this IViewableMap<K, V> me, Lifetime lifetime, Action<AddRemove, K, V?> handler) where K : notnull
{
me.Advise(lifetime, e =>
{
switch (e.Kind)
{
case AddUpdateRemove.Add:
handler(AddRemove.Add, e.Key, e.NewValue);
break;
case AddUpdateRemove.Update:
handler(AddRemove.Remove, e.Key, e.OldValue);
handler(AddRemove.Add, e.Key, e.NewValue);
break;
case AddUpdateRemove.Remove:
handler(AddRemove.Remove, e.Key, e.OldValue);
break;
default:
throw new ArgumentOutOfRangeException("" + e.Kind);
}
});
}
public static void Advise<T>(this IViewableSet<T> me, Lifetime lifetime, Action<AddRemove, T> handler) where T: notnull
{
me.Advise(lifetime, e => handler(e.Kind, e.Value));
}
public static void View<T>(this IViewableSet<T> me, Lifetime lifetime, Action<Lifetime, T> handler) where T : notnull
{
var lifetimes = new Dictionary<T, LifetimeDefinition>(me.Count);
me.Advise(lifetime, (kind, value) =>
{
switch (kind)
{
case AddRemove.Add:
var def = Lifetime.Define(lifetime);
lifetimes[value] = def;
handler(def.Lifetime, value);
break;
case AddRemove.Remove:
def = lifetimes[value];
lifetimes.Remove(value);
def.Terminate();
break;
default:
throw new ArgumentOutOfRangeException($"illegal enum value: {kind}");
}
});
}
public static void View<K, V>(this IViewableMap<K, V> me, Lifetime lifetime, Action<Lifetime, K, V> handler) where K : notnull
{
#nullable disable
var lifetimes = new Dictionary<KeyValuePair<K, V>, LifetimeDefinition>();
me.AdviseAddRemove(lifetime, (kind, key, value) =>
{
var entry = JetKeyValuePair.Of(key, value);
switch (kind)
{
case AddRemove.Add:
var def = Lifetime.Define(lifetime);
lifetimes.Add(entry, def);
handler(def.Lifetime, entry.Key, entry.Value);
break;
case AddRemove.Remove:
def = lifetimes[entry];
lifetimes.Remove(entry);
def.Terminate();
break;
default:
throw new ArgumentOutOfRangeException($"Illegal enum value: {kind}");
}
});
#nullable enable
}
/// <summary>
/// Same as <see cref="ISource{T}.Advise"/> but all events with kind=<see cref="AddUpdateRemove.Update"/> are
/// split into two sequential events with kinds <see cref="AddUpdateRemove.Remove"/> and <see cref="AddUpdateRemove.Add"/>
/// </summary>
/// <param name="me"></param>
/// <param name="lifetime"></param>
/// <param name="handler"></param>
/// <typeparam name="V"></typeparam>
/// <exception cref="ArgumentOutOfRangeException"></exception>
public static void AdviseAddRemove<V>(this IViewableList<V> me, Lifetime lifetime, Action<AddRemove, int, V> handler) where V : notnull
{
#nullable disable
me.Advise(lifetime, e =>
{
switch (e.Kind)
{
case AddUpdateRemove.Add:
handler(AddRemove.Add, e.Index, e.NewValue);
break;
case AddUpdateRemove.Update:
handler(AddRemove.Remove, e.Index, e.OldValue);
handler(AddRemove.Add, e.Index, e.NewValue);
break;
case AddUpdateRemove.Remove:
handler(AddRemove.Remove, e.Index, e.OldValue);
break;
default:
throw new ArgumentOutOfRangeException($"Illegal enum value: {e.Kind}");
}
});
#nullable enable
}
/// <summary>
/// Structured subscription for List. Behaves same as <see cref="ISource{T}.Advise"/>
/// except <c>handler</c> receives <see cref="Lifetime"/> parameter
/// that terminates when someone removes or updates element on corresponding index.
/// </summary>
/// <param name="me"></param>
/// <param name="lifetime"></param>
/// <param name="handler"></param>
/// <typeparam name="V"></typeparam>
/// <exception cref="ArgumentOutOfRangeException"></exception>
public static void View<V>(this IViewableList<V> me, Lifetime lifetime, Action<Lifetime, int, V> handler) where V : notnull
{
var lifetimes = new List<LifetimeDefinition>(me.Count);
me.AdviseAddRemove(lifetime, (kind, index, value) =>
{
switch (kind)
{
case AddRemove.Add:
var def = Lifetime.Define(lifetime);
lifetimes.Insert(index, def);
handler(def.Lifetime, index, value);
break;
case AddRemove.Remove:
def = lifetimes[index];
lifetimes.RemoveAt(index);
def.Terminate();
break;
default:
throw new ArgumentOutOfRangeException($"Illegal enum value: {kind}");
}
});
}
public static bool HasValue<T>(this IReadonlyProperty<T> me)
{
return me.Maybe.HasValue;
}
public static bool HasTrueValue(this IReadonlyProperty<bool> me)
{
return me.Maybe.HasValue && me.Maybe.Value;
}
public static void Compose<T1, T2>(this IReadonlyProperty<T1> first, Lifetime lifetime, IReadonlyProperty<T2> second, Action<T1, T2> composer)
{
first.Advise(lifetime, v =>
{
if (second.HasValue()) composer(v, second.Value);
});
second.Advise(lifetime, v =>
{
if (first.HasValue()) composer(first.Value, v);
});
}
public static IReadonlyProperty<T> Compose<T1, T2, T>(this IReadonlyProperty<T1> first, Lifetime lifetime, IReadonlyProperty<T2> second, Func<T1, T2, T> composer)
{
var res = new ViewableProperty<T>();
first.Advise(lifetime, v =>
{
if (second.HasValue()) res.Value = composer(v, second.Value);
});
second.Advise(lifetime, v =>
{
if (first.HasValue()) res.Value = composer(first.Value, v);
});
return res;
}
public static void WhenTrue(this IReadonlyProperty<bool> property, Lifetime lifetime, Action<Lifetime> handler)
{
if (property == null) throw new ArgumentNullException(nameof(property));
if (handler == null) throw new ArgumentNullException(nameof(handler));
property.View(lifetime, (lf, value) =>
{
if (value) handler(lf);
});
}
public static void WhenFalse(this IReadonlyProperty<bool> property, Lifetime lifetime, Action<Lifetime> handler)
{
if (property == null) throw new ArgumentNullException(nameof(property));
if (handler == null) throw new ArgumentNullException(nameof(handler));
property.View(lifetime, (lf, value) =>
{
if (!value) handler(lf);
});
}
private class MappedSink<T, R> : ISource<R>
{
private readonly ISource<T> myOriginal;
private readonly Func<T, R> myMap;
public MappedSink(ISource<T> original, Func<T, R> map)
{
myOriginal = original;
myMap = map;
}
public void Advise(Lifetime lifetime, Action<R> handler)
{
myOriginal.Advise(lifetime, x => handler(myMap(x)));
}
}
private class MappedProperty<T, R> : IReadonlyProperty<R>
{
private readonly IViewableProperty<T> mySource;
private readonly Func<T, R> myMap;
public MappedProperty(IViewableProperty<T> source, Func<T, R> map)
{
mySource = source;
myMap = map;
Change = new MappedSink<T, R>(source.Change, myMap);
}
public void Advise(Lifetime lifetime, Action<R> handler) => mySource.Advise(lifetime, v => handler(myMap(v)));
public ISource<R> Change { get; }
public Maybe<R> Maybe => mySource.Maybe.Select(myMap);
public R Value => Maybe.Value;
}
public static IReadonlyProperty<R> Select<T, R>(this IViewableProperty<T> source, Func<T, R> f)
{
return new MappedProperty<T,R>(source, f);
}
public static Task<T> NextNotNullValueAsync<T>(this ISource<T> source, Lifetime lifetime)
{
return source.NextValueAsync(lifetime, value => value != null);
}
public static Task<bool> NextTrueValueAsync(this ISource<bool> source, Lifetime lifetime)
{
return source.NextValueAsync(lifetime, value => value);
}
public static Task<bool> NextFalseValueAsync(this ISource<bool> source, Lifetime lifetime)
{
return source.NextValueAsync(lifetime, value => !value);
}
public static Task<T> NextValueAsync<T>(this ISource<T> source, Lifetime lifetime)
{
return source.NextValueAsync(lifetime, _ => true);
}
public static Task<T> NextValueAsync<T>(this ISource<T> source, Lifetime lifetime, Func<T, bool> condition)
{
var tcs = new TaskCompletionSource<T>();
var definition = lifetime.CreateNested();
definition.SynchronizeWith(tcs);
source.Advise(definition.Lifetime, v =>
{
if (condition(v))
tcs.TrySetResult(v);
});
return tcs.Task;
}
}
}