Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Actor Feed Loaders #70

Open
wants to merge 19 commits into
base: master
Choose a base branch
from
Open

Actor Feed Loaders #70

wants to merge 19 commits into from

Conversation

EricBAndrews
Copy link
Member

@EricBAndrews EricBAndrews commented Nov 8, 2024

This PR completely reworks the FeedLoader architecture to use native Swift actors for concurrency management. This is achieved by the addition of two new components: the LoadingActor and the Fetcher.

LoadingActor

The LoadingActor is an actor that is responsible for loading. Because actors are extremely inflexible, this is designed to be as minimal and generic as possible; it can be thought of as the "kernel" of the loading system. The LoadingActor defines two operations:

  • load: loads the next page of items
  • reset: resets the loading state

The behavior of these operations is subject to the following behavior requirements:

  1. It should be possible to call both reset and load while an existing load call is awaiting the server response
  2. If reset is called while the actor is loading, the existing load should be immediately cancelled
  3. If load is called while the actor is loading, the new load call should be ignored

These requirements ensure that the frontend is responsive (e.g., refreshing the feed does not require waiting for an existing load to complete just to be thrown out) and duplicate loads (e.g., as caused by scrolling fast and hitting both the normal and the fallback threshold) are ignored.

To support this behavior, the LoadingActor stores a loadingTask. This is an independent Task that performs the actual server call; if no server call is in progress, loadingTask is nil. load spawns this task then awaits its result; the await operation frees up the actor to handle new reset or load calls. This architecture satisfies the above requirements:

  1. New load or reset operations can be handled as soon as the loadingTask is spawned.
  2. If a reset call comes in while loadingTask is non-nil, it simply calls Task.cancel() on the loadingTask. That loadingTask will return a cancelled status.
  3. If a load call comes in while loadingTask is non-nil, load returns the ignored status.

Fetcher

Swift does not allow inheritance with actors. This renders LoadingActor highly inflexible; each fetch use cases requires tracking a unique set of fetch parameters, which is inelegant to handle within the LoadingActor itself. LoadingActor is therefore instantiated with a Fetcher: a class which provides the fetchPage and fetchCursor methods and handles the unique fetch parameters for each loader. The Fetcher is stored within the FeedLoader and passed as a reference into the LoadingActor, allowing the FeedLoader to modify fetching behavior (e.g., change the query string for a search fetch).

Responsibility Changes

The FeedLoader looks largely the same as before, but with much of its operation now delegated to the Fetcher and the LoadingActor. Most notably, the FeedLoader's loadingState no longer has concurrency management responsibilities. The responsibilities of each component are as follows:

FeedLoader Fetcher LoadingActor
Filtering, custom post-fetch processing (e.g., image prefetching), and handling frontend requests Tracking fetch parameters and defining fetching behavior Concurrency management

@EricBAndrews EricBAndrews marked this pull request as ready for review November 13, 2024 23:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant