|
2 | 2 | // Roland Pheasant licenses this file to you under the MIT license.
|
3 | 3 | // See the LICENSE file in the project root for full license information.
|
4 | 4 |
|
| 5 | +using System.Diagnostics; |
5 | 6 | using System.Reactive.Disposables;
|
6 | 7 | using System.Reactive.Linq;
|
7 | 8 | using DynamicData.Internal;
|
8 | 9 |
|
9 | 10 | namespace DynamicData.Cache.Internal;
|
10 | 11 |
|
11 |
| -internal sealed class TransformOnObservable<TSource, TKey, TDestination>(IObservable<IChangeSet<TSource, TKey>> source, Func<TSource, TKey, IObservable<TDestination>> transform) |
| 12 | +internal sealed class TransformOnObservable<TSource, TKey, TDestination>(IObservable<IChangeSet<TSource, TKey>> source, Func<TSource, TKey, IObservable<TDestination>> transform, bool transformOnRefresh = false) |
12 | 13 | where TSource : notnull
|
13 | 14 | where TKey : notnull
|
14 | 15 | where TDestination : notnull
|
15 | 16 | {
|
16 |
| - public IObservable<IChangeSet<TDestination, TKey>> Run() => Observable.Create<IChangeSet<TDestination, TKey>>(observer => |
| 17 | + public IObservable<IChangeSet<TDestination, TKey>> Run() => |
| 18 | + Observable.Create<IChangeSet<TDestination, TKey>>(observer => new Subscription(source, transform, observer, transformOnRefresh)); |
| 19 | + |
| 20 | + // Maintains state for a single subscription |
| 21 | + private sealed class Subscription : IDisposable |
17 | 22 | {
|
18 |
| - var cache = new ChangeAwareCache<TDestination, TKey>(); |
19 |
| - var locker = InternalEx.NewLock(); |
20 |
| - var parentUpdate = false; |
| 23 | +#if NET9_0_OR_GREATER |
| 24 | + private readonly Lock _synchronize = new(); |
| 25 | +#else |
| 26 | + private readonly object _synchronize = new(); |
| 27 | +#endif |
| 28 | + private readonly ChangeAwareCache<TDestination, TKey> _cache = new(); |
| 29 | + private readonly KeyedDisposable<TKey> _transformSubscriptions = new(); |
| 30 | + private readonly Func<TSource, TKey, IObservable<TDestination>> _transform; |
| 31 | + private readonly IDisposable _sourceSubscription; |
| 32 | + private readonly IObserver<IChangeSet<TDestination, TKey>> _observer; |
| 33 | + private readonly bool _transformOnRefresh; |
| 34 | + private int _subscriptionCounter = 1; |
| 35 | + private int _updateCounter; |
| 36 | + |
| 37 | + public Subscription(IObservable<IChangeSet<TSource, TKey>> source, Func<TSource, TKey, IObservable<TDestination>> transform, IObserver<IChangeSet<TDestination, TKey>> observer, bool transformOnRefresh) |
| 38 | + { |
| 39 | + _observer = observer; |
| 40 | + _transform = transform; |
| 41 | + _transformOnRefresh = transformOnRefresh; |
| 42 | + _sourceSubscription = source |
| 43 | + .Do(_ => IncrementUpdates()) |
| 44 | + .Synchronize(_synchronize) |
| 45 | + .SubscribeSafe(ProcessSourceChangeSet, observer.OnError, CheckCompleted); |
| 46 | + } |
| 47 | + |
| 48 | + public void Dispose() |
| 49 | + { |
| 50 | + lock (_synchronize) |
| 51 | + { |
| 52 | + _sourceSubscription.Dispose(); |
| 53 | + _transformSubscriptions.Dispose(); |
| 54 | + } |
| 55 | + } |
21 | 56 |
|
22 |
| - // Helper to emit any pending changes when appropriate |
23 |
| - void EmitChanges(bool fromParent) |
| 57 | + private void ProcessSourceChangeSet(IChangeSet<TSource, TKey> changes) |
24 | 58 | {
|
25 |
| - if (fromParent || !parentUpdate) |
| 59 | + // Process all the changes at once to preserve the changeset order |
| 60 | + foreach (var change in changes.ToConcreteType()) |
26 | 61 | {
|
27 |
| - var changes = cache!.CaptureChanges(); |
| 62 | + switch (change.Reason) |
| 63 | + { |
| 64 | + // Shutdown existing sub (if any) and create a new one that |
| 65 | + // Will update the cache and emit the changes |
| 66 | + case ChangeReason.Add or ChangeReason.Update: |
| 67 | + CreateTransformSubscription(change.Current, change.Key); |
| 68 | + break; |
| 69 | + |
| 70 | + // Shutdown the existing subscription and remove from the cache |
| 71 | + case ChangeReason.Remove: |
| 72 | + _transformSubscriptions.Remove(change.Key); |
| 73 | + _cache.Remove(change.Key); |
| 74 | + break; |
| 75 | + |
| 76 | + case ChangeReason.Refresh: |
| 77 | + if (_transformOnRefresh) |
| 78 | + { |
| 79 | + CreateTransformSubscription(change.Current, change.Key); |
| 80 | + } |
| 81 | + else |
| 82 | + { |
| 83 | + // Let the downstream decide what this means |
| 84 | + _cache.Refresh(change.Key); |
| 85 | + } |
| 86 | + break; |
| 87 | + } |
| 88 | + } |
| 89 | + |
| 90 | + // Emit any pending changes |
| 91 | + EmitChanges(); |
| 92 | + } |
| 93 | + |
| 94 | + private void IncrementUpdates() => Interlocked.Increment(ref _updateCounter); |
| 95 | + |
| 96 | + private void EmitChanges() |
| 97 | + { |
| 98 | + if (Interlocked.Decrement(ref _updateCounter) == 0) |
| 99 | + { |
| 100 | + var changes = _cache.CaptureChanges(); |
28 | 101 | if (changes.Count > 0)
|
29 | 102 | {
|
30 |
| - observer.OnNext(changes); |
| 103 | + _observer.OnNext(changes); |
31 | 104 | }
|
| 105 | + } |
32 | 106 |
|
33 |
| - parentUpdate = false; |
| 107 | + Debug.Assert(_updateCounter >= 0, "Should never be negative"); |
| 108 | + } |
| 109 | + |
| 110 | + private void CheckCompleted() |
| 111 | + { |
| 112 | + if (Interlocked.Decrement(ref _subscriptionCounter) == 0) |
| 113 | + { |
| 114 | + _observer.OnCompleted(); |
34 | 115 | }
|
| 116 | + |
| 117 | + Debug.Assert(_subscriptionCounter >= 0, "Should never be negative"); |
35 | 118 | }
|
36 | 119 |
|
37 | 120 | // Create the sub-observable that takes the result of the transformation,
|
38 | 121 | // filters out unchanged values, and then updates the cache
|
39 |
| - IObservable<TDestination> CreateSubObservable(TSource obj, TKey key) => |
40 |
| - transform(obj, key) |
| 122 | + private void CreateTransformSubscription(TSource obj, TKey key) |
| 123 | + { |
| 124 | + // Add a new subscription. Do first so cleanup of existing subs doesn't trigger OnCompleted. |
| 125 | + Interlocked.Increment(ref _subscriptionCounter); |
| 126 | + |
| 127 | + // Create a container for the Disposable and add to the KeyedDisposable |
| 128 | + var disposableContainer = _transformSubscriptions.Add(key, new SingleAssignmentDisposable()); |
| 129 | + |
| 130 | + // Create the transformation observable for the source item, filter unchanged, and update the cache |
| 131 | + // Will Dispose immediately if OnCompleted fires upon subscription because OnCompleted disposes the container |
| 132 | + // Remove the TransformSubscription if it completes because its not needed anymore |
| 133 | + disposableContainer.Disposable = _transform(obj, key) |
41 | 134 | .DistinctUntilChanged()
|
42 |
| - .Synchronize(locker!) |
43 |
| - .Do(val => cache!.AddOrUpdate(val, key)); |
44 |
| - |
45 |
| - // Flag a parent update is happening once inside the lock |
46 |
| - var shared = source |
47 |
| - .Synchronize(locker!) |
48 |
| - .Do(_ => parentUpdate = true) |
49 |
| - .Publish(); |
50 |
| - |
51 |
| - // MergeMany automatically handles Add/Update/Remove and OnCompleted/OnError correctly |
52 |
| - var subMerged = shared |
53 |
| - .MergeMany(CreateSubObservable) |
54 |
| - .SubscribeSafe(_ => EmitChanges(fromParent: false), observer.OnError, observer.OnCompleted); |
55 |
| - |
56 |
| - // Subscribe to the shared Observable to handle Remove events. MergeMany will unsubscribe from the sub-observable, |
57 |
| - // but the corresponding key value needs to be removed from the Cache so the remove is observed downstream. |
58 |
| - var subRemove = shared |
59 |
| - .OnItemRemoved((_, key) => cache!.Remove(key), invokeOnUnsubscribe: false) |
60 |
| - .SubscribeSafe(_ => EmitChanges(fromParent: true), observer.OnError); |
61 |
| - |
62 |
| - return new CompositeDisposable(shared.Connect(), subMerged, subRemove); |
63 |
| - }); |
| 135 | + .Do(_ => IncrementUpdates()) |
| 136 | + .Synchronize(_synchronize) |
| 137 | + .Finally(CheckCompleted) |
| 138 | + .SubscribeSafe(val => TransformOnNext(val, key), _observer.OnError, () => _transformSubscriptions.Remove(key)); |
| 139 | + } |
| 140 | + |
| 141 | + private void TransformOnNext(TDestination latestValue, TKey key) |
| 142 | + { |
| 143 | + _cache.AddOrUpdate(latestValue, key); |
| 144 | + EmitChanges(); |
| 145 | + } |
| 146 | + } |
64 | 147 | }
|
0 commit comments