mlemgroup / mlem

Mlem for Lemmy
https://lemmy.ml/c/mlemapp
GNU General Public License v3.0
176 stars 32 forks source link

2.0: Rewrite API and Middleware #73

Open EricBAndrews opened 1 year ago

EricBAndrews commented 1 year ago

Create a set of enriched data models to feed to the frontend, decoupling our frontend code from the API spec and enabling features such as:

EricBAndrews commented 7 months ago

From this issue:

What are we using now?

'Middleware' is the logic that connects the front-end (what the user sees) to the API via the APIClient class.

Currently, views that need to handle various types of content (such as posts) have a StateObject ContentTracker property that stores an array of ContentModel items inside of it. ContentModel is a protocol to which several individual types conform - PostModel, UserModel, CommunityModel etc. These are all structs, and are therefore immutable.

Changes are made to a model via mutating methods of those models. For example, CommunityModel has a toggleSubscribe method. When such a method of a ContentModel type is called, it updates its own properties to reflect the requested change and sends a request to the Lemmy API to perform the change on their end.

Problems with this implementation

Because ContentModel types are value types and not reference types, we need to update the relevant trackers whenever a change is made.

To do this, mutating methods of ContentModel types accept a trailing closure that is called whenever the ContentModel makes a change to itself. Inside of this trailing closure, we can provide logic for updating the relevant trackers. This closure is usually called more than once in the method call.

Here's the syntax for this:

// This would be in a view

@EnvironmentObject var tracker: ContentTracker<CommunityTracker>
var community: CommunityModel

func toggleSubscribe() {
    community.toggleSubscribe {
        tracker.update(with: $0)
    }
}

This syntax is rather cumbersome, especially in more complex situations. If a tracker contains an array of PostModels, and the user bans the author of one post, it would be tricky to update every post authored by that user to reflect the change.

How do we fix this?

If ContentModel types were classes instead of structs, we could simply make changes to the model instance without worrying about having to update it across all trackers.

For example, if two separate PostModel instances have the same author, the author property for both posts should reference the same instance of UserModel. This means that any changes we make to the UserModel instance are reflected everywhere in the app without us having to do any extra work. To do this, we'd need a caching system that ensures that the same model instance is used to represent each user/post/community.

This is far, far easier to do if we can use @Observable from iOS 17. I've made a mock-up implementation of what this system could look like here:

https://github.com/Sjmarf/mlem-middleware-mockup

EricBAndrews commented 7 months ago

https://github.com/mlemgroup/mlem/blob/sjmarf/middleware-rewrite-2/Mlem/Models/Content/NewCommunity/README.md

EricBAndrews commented 7 months ago

Renamed to "Rewrite API and Middleware"