Feeds architecture

This document gives information about the architecture and the implementation of feeds. To get information about the usage off the feeds framework, you should look to this doc.

Dev general guidelines

  • All instances of feeds should be cached using the AttachedProperty helper.
  • Consequently, all feed implementation must be state less (except the special case of State<T>).
  • When invoking a user asynchronous method, the SourceContext should be set as ambient (cf. FeedHelper.InvokeAsync).
  • Untyped interfaces (IMessage, IMessageEntry, etc.) exists only for binding consideration and should not be re-implemented nor used in source code.

Caching

In order to allow a light creation syntax in a property getter (public Feed<int> MyFeed => _anotherFeed.Select(_ => 42), which is re-evaluated each time the property is get), but without rebuilding and re-querying the feed each time, we have 2 levels of caching.

Instance caching

First is the instance of the feed itself. This is done using the AttachedProperty helper class. Each feed if attached to a owner and identified by a key. It makes sure that for a given owner we have only one instance of the declared feed, i.e. running _anotherFeed.Select(_ => 42) will always return the same instance.

The owner is usually (by order of preference):

  1. The parent feed if any (_anotherFeed in example above);
  2. The Target of the key delegate, so the instance of the class that is declaring the feed;
  3. The key delegate itself if it’s a static delegate instance (e.g., in the example above _ => 42 the Target is going to be null as we don’t have any capture)

The key is usually the delegate that is provided by the user. It’s really important here to note that a helper method like below would break the instance caching:

public static Feed<string> ToString(this IFeed<T> feed, Func<T, string> toString)
  => feed.Select(t => toString(t));

As the delegate t => toString(t) is declared in the method itself, it will be re-instantiated each time. Valid implementations would have been:

public static Feed<string> ToString(this IFeed<T> feed, Func<T, string> toString)
  => feed.Select(toString); // We are directly forwarding the user delegate
public static Feed<string> ToString(this IFeed<T> feed, Func<T, string> toString)
  => AttachedProperty.GetOrCreate(owner: feed, key: toString, factory: (theFeed, theToString) => theFeed.Select(t => theToString(t))); // We are explicitly caching the instance.

Subscription caching

The second level of caching is for the “subscription” on a feed. This is needed to make sure that in a given context, enumerating / awaiting multiple times to a same feed won’t re-build the value (noticeably, won’t re-request a value coming from a web API call).

This caching is achieved by the SourceContext.

Those context are weakly attached to a owner (typically a ViewModel) and each call to context.GetOrCreateSource(feed) will return a state full subscription to the feed which will replay the last received message.

When implementing an IFeed, the context provided in the GetSource is only intended to be used to restore it as current in some circumstances, like invoking a user’s async method. Your feed must remain state less, so you should not use context.GetOrCreateSource(parent).

On the other side, each helper that allow user to “subscribe” to a feed should do something like SourceContext.Current.GetOrCreateSource(feed) (and not feed.GetSource(SourceContext.Current))

Issuing messages

When implementing an IFeed you will have to create some messages.

If you don't have any parent feed the easiest way is to start from Message<T>.Initial (do not send it as first message), then update it:

var current = Message<int>.Initial;
for (var i = 0; i++; i < 42)
{
  yield return i;
  await Task.Delay(100, ct);
}

If you do have a parent feed, you should use the MessageManager<TParent, TResult>, eg.:

var manager = new MessageManager<TParent, TResult>();
var msgIndex = 0;
await foreach(var parentMsg in _parent.GetSource(context))
{
  if (manager.Update(localMsg => localMsg.With(parentMsg).Data(msgIndex++)))
  {
    yield return manager.Current;
  }
}

Make sure that your feed always produces at least on message. If there isn’t any relevant, send the Message.Initial before completing the IAsyncEnumerable source.

If you have a parent feed make sure to always forward the parent message, even if the parent message does not change any local value: manager.Update(localMsg => localMsg.With(parentMsg)).

Be aware that enumeration of an IAsyncEnumerable is sequential (i.e. one value at once). The MessageManager also has a constructor that allows to asynchronously send messages to an AsyncEnumerableSubject.

Axes

An axe is referring to an “informational axe” related to a given data, a.k.a. a metadata. Currently the feed framework is managing (i.e., actively generating value for) only 2 metadata: error and progress, but as Messages are designed to encapsulate a data and all its metadata, a MessageEntry can have more than those 2 well-known axes.

┌────┐1   *┌────────┐    2┌────────────┐1   *┌────────────────┐
│Feed├────►│Message │  ┌─►│MessageEntry├────►│MessageAxis     │
└────┘     ├────────┤  │  ├────────────┤     └─────┬──────────┘
           │Previous├──┤  │Data        │1          │1
           │Current ├──┘  │Error       ├──┐        │
           │Changes │     │Progress    │  │        │1
           └────────┘     └────────────┘  │ *┌─────┴──────────┐
                                          └─►│MessageAxisValue│
                                             └────────────────┘

MessageEntry are basically dictionaries of MessageAxis and MessageAxisValue, except they are returning MessageAxisValue.Unset instead of throwing error for unset axes.

The DataAxis is a special axis that must be set on all entries. It exists only to unify/ease implementation. You should use Option.Undefined to “unset” the data.

If you are about to add an axe, you should make sure to provided extensions methods over IMessageBuilder and IMessageEntry to read/write it directly to/from the effective metadata type. The generic Get and Set are there for that and should not be used directly in user’s code.

Request

The subscriber of a feed can send some request to the source feed to enhance its behavior. The most common request is the RefreshRequest.

When implementing an IFeed you have access to those requests using the Requests<TRequest>() method on the SourceContext you get in the GetSource.

When consuming a feed, you can send a request to that feed by creating a "child" context (SourceContext.CreateChild()) giving you own IRequestSource.

Token and TokenSet

In a response to a request a feed might issue a token that is then added to its messages so the subscriber that sent the request is able to track the completion of the request. This is the case for the Refresh and the Pagination which are forwarding those tokens through the refresh and the pagintation axes.

As when a subscriber sends a request, it might be handled by more than one feed. For instance, when combining two AsyncFeed instances, the refresh request will cause those two feeds to refresh their data. Even if it's not yet the implemented, we can also imagine that an operator feed (such as the SelectAsyncFeed) might also trigger a refresh of its own projection. Refresh and Pagination axes are working with TokenSet. It is a collection of IToken that only keep the latest tokens for a given source in relation to the subscription context.

        Subscriber       Combine     Async A     Async B

            │               │           │           │
       ┌─ Control channel (Requests on SourceContext) ─┐
       │    │               │           │           │  │
       │    │   Request 1   │           │           │  │
    ┌──┼──◄─┼──────────────►│ Request 1 │           │  │
    │  │    │               ├──────────►│           │  │
    ▼  │    │               │  Token A  │           │  │
    │  │    │               │◄──────────┘           │  │
    │  │    │               │       Request 1       │  │
    ▼  │    │               ├──────────────────────►│  │
    │  │    │               │        Token B        │  │
    │  │    │               │◄──────────┬───────────┤  │
    ▼  │    │ TokenSet[A,B] │           │           │  │
    │  │    │◄──────────────┤           │           │  │
    │  │    │               │           │           │  │
    ▼  └────┼───────────────┼───────────┼───────────┼──┘
IsExecuting │               │           │           │
    │  ┌─ Message channel ──┼───────────┼───────────┼──┐
    │  │    │               │           │           │  │
    ▼  │    │               ┤ Msg with  │           │  │
    │  │    │               │  token A  │           │  │
    │  │    │               │◄──────────┤ Msg with  │  │
    ▼  │    │   Msg with    │           │  token A  │  │
    │  │    │ TokenSet[A,B] │◄──────────┼───────────┤  │
    └─►├───►│◄──────────────┤           │           │  │
       │    │               │           │           │  │
       └────┼───────────────┼───────────┼───────────┼──┘
            │               │           │           │
Note

As a subscriber, you can use the TokenSetAwaiter to wait for a set that has been produced by a request.

Note

When implementing an IFeed you can use the CoercingRequestManager or the SequentialRequestManager to easily implement such request / token logic.

When you answer to a request with a token, you must then issue a new message with that token.

View

While developing in the feed framework, the most interesting view-related part is the presentation layer. Feeds are transferring messages containing the data and its metadata, but those are not binding friendly. The presentation layer has then 2 responsibilities:

  1. Read, i.e. from the ViewModel to the View, it will expose and maintain the state of the last message through standard binding-friendly properties and INotifyPropertyChanged;
  2. Write, i.e. from the View to the ViewModel, it will convert back standard properties into so called IInput, which inherits from the IFeed interface, and which allows the ViewModel to manage the view just like any other data source.

Those binding friendly properties are generated by the FeedsGenerator.

To trigger the generation, the class needs to have at least on constructor which has an IInput, or an ICommandBuilder parameter, or flag the type with [ReactiveBindable(true)] attribute.

When we generate a binding friendly class for a type MyType we are generating a BindableMyType class nested into the MyType itself. This class inherits from the BindableViewModelBase.

Denormalizing

In order to make a record editable field per field (e.g. a user profile where you want to edit first name and last name independently), we can generate a BindableRecord object that we re-expose each property of the Record independently.

When a record is denormalized, we are generating / maintaining only one State<Record>. Setting a value of any of the denormalized properties, will update that root state.

When a record is denormalized, we can no longer directly set a record instance on the VM.Record (the property is of type BindableRecord). To work around this, if the record does not have a Value property which would conflict, we are generating a get/set property Value which can be used to set the instance (e.g. VM.Record.Value).

When a property is updated, we also ensure to raise the property changes, including for the Value property (if defined) and the GetValue method.

Dispatching and threading considerations

The user’s ViewModel are expected to run on background thread for performance considerations. Commands and state updates from generated ViewModel classes are always ran on a background thread.

To avoid strong dependency on a dispatcher (neither by injection nor by the need to create VM on UI thread), all generated classes are auto resolving the dispatcher when an event handler is registered or when a public method is used, using the internal LazyDispatcherResolver and EventManager. Those classes are then completing their initialization once the first dispatcher is resolved.

The EventManager will capture the thread used to register an event handler and will then make sure to invoke that handler on that thread if it was a UI thread. This allows BindableVM to be data-bound to multiple window/UI threads.

┌───────────────┐     ┌───────────────┐
│EventManager   │     │IInvocationList│
├───────────────┤1   *├───────────────┤
│Add(handler)   ├────►│Add(handler)   │
│Remove(handler)│     │Remove(handler)│
│Raise(args)    │     │Raise(args)    │
└───────────────┘     └───────▲───────┘
                              │
              ┌───────────────┴──────────────┐
              │                              │
  ┌───────────┴────────────┐    ┌────────────┴────────────┐
  │DispatcherInvocationList│    │MultiThreadInvocationList│
  └───────────▲────────────┘    └─────────────────────────┘
              │
        ┌─────┴───────┐
        │             │
  ┌─────┴─────┐   ┌───┴────┐
  │Coaslescing│   │Queueing│
  └───────────┘   └────────┘

We have one IInvocationList per UI thread and one for all background threads (the MultiThreadInvocationList).

As a bonus, when a dispatcher bound handler is about to be invoked from another thread, it can be coalesced to be raised only once (cf. CoalescingDispatcherInvocationList).