borchero / Squid

Declarative and Reactive Networking for Swift.
https://squid.borchero.com
MIT License
71 stars 6 forks source link

Ideas on how to implement a caching layer #10

Closed SilverTab closed 4 years ago

SilverTab commented 4 years ago

I'm trying to add a caching layer to my service (the url building/fetching/etc. is done with Squid). The obvious thing to do would be to use request URLs as cache keys, and return results from the cache before scheduling requests, however, the fact that all the URL logic is abstracted away (by HttpRoute, HttpQuery etc.) makes it hard. I tried to hack into the "prepare" function but couldn't really come up with a solution.

I was wondering if you had any ideas on how I could achieve such a goal? Any ideas/pointers would be appreciated!

borchero commented 4 years ago

So first of all, as long as you define your requests as struct, you should be able to tell them apart properly for caching just by adding conformance to Equatable (the == function will be inferred by the compiler). Then, your caching layer could have some function check<E>(_ value: E) where E: Equatable and e.g. store some dictionary [Equatable: Any] (probably, you can do better than Any with some thinking).

Then, my idea would be (and I would need to implement this if you like the idea, but the use case sounds very frequent) to allow "hooks" that you define on a service-level. When a request is scheduled, it passes the request to the service's hook and if that hook does not return an error (i.e. some NotFound error), it immediately returns the returned value without scheduling the request.

How does that sound to you?

SilverTab commented 4 years ago

Sounds good to me! I'll keep an eye out on future commits for the "hook" functionality if you decide to go ahead and implement it! :)

SilverTab commented 4 years ago

On second thought, considering my requests have a header that changes every time the request is made, I'm not sure if being Equatable is going to be enough for me to be able to retrieve it from the cache 🤔I might have to consider a different way to implement this...

borchero commented 4 years ago

So my current approach would be to establish the following protocol:

public protocol ServiceHook {

    func processRequest<R>(_ request: R, urlRequest: URLRequest) -> R.Result? where R: Request

    func processResponse<R>(of request: R, urlRequest: URLRequest, response: Result<R.Result, Error>) where R: Request
}

It's totally up to you how to implement equality of requests then. It will take a bit though to be implemented as I'll have to change the way retriers are implemented internally.

SilverTab commented 4 years ago

Very nice! I feel like this would offer enough flexibility to cover most use-cases I can think of!

I feel bad for giving you all this trouble, and, possibly forcing you to move slightly away from the "declarative" nature of Squid, but the always-changing header field and the caching layer are 2 real constraints I have to deal with and really couldn't think of better ways to implement without some very ugly hacks, or without forking Squid, both which I'm glad to avoid if possible!

That being said, take all the time you need for the implementation, I'm glad I'll be able to stick with Squid in the project I'm working on as I feel like its simplicity and design goals fit very well with what I'm trying to accomplish, and with SwiftUI in general :)

borchero commented 4 years ago

Done :) https://borchero.github.io/Squid/Protocols/ServiceHook.html

There's even a caching hook already implemented: https://borchero.github.io/Squid/Classes/CachingServiceHook.html

SilverTab commented 4 years ago

Amazing! :)

While not really necessary for my use-case, one typical implementation for networking libraries that allow caching is to return a cached result immediately, while still executing the request in the background and updating the cached entry. So you get an immediate result from the cache, but you also update your cache with the newest data from your API. I still haven't looked at your implementation in details to know if such a thing would be possible, perhaps it would be doable at a higher level, by chaining the right combine operators!

borchero commented 4 years ago

Sounds interesting, but I'm not sure if that functionality is really required. When you have some reactive store (which is outside the realm of this library), you can just subscribe to it and then schedule a network request to update that store. You wouldn't even need Squid's caching functionality. I felt like this kind of caching is focused on actually preventing calls made to the server.

So in case you want to do something like you described, I would rather recommend a proper reactive database (such as Realm) or something like SwiftRex for smaller projects.

SilverTab commented 4 years ago

That makes sense. I'm still more used to a more "imperative" way of doing things (and I'm trying to rewrite a very old, Objective-C networking layer), and the flow I described is the typical caching behavior for a couple of more traditional networking libs I've looked at (Siesta does it that way for example), so I figured I'd mention it, but suspected it was out of the scope of this project. Reactive/functional paradigms take some getting used to :) I appreciate all your insights!