Dependencies Between Environment Clients

Hello everyone.

What is the recommended way to share functionality between Environment clients?

I'm currently developing a way to create, access and cache attachments for a feature. I find myself wanting to create an AttachmentsClient (with a .live and .mock extensions) and leverage that in the environment. However, other clients in the environment would likely want to access this functionality as well, and do so while accessing details specific to their implementation.

For example, a hypothetical .live WidgetsClient implementation could provide a list of WidgetRowStates as an Effect by querying documents in a local, no-sql database. As part of transforming the database query results into row states, it would also transform attachment identifiers into remote or local cache URLs to display widget photos in the list.

It's likely those implementation query results shouldn't leak out of the client into the state. Even if that was not specific to the live implementation, I don't think I'd want to receive the row state results from the WidgetClient in the reducer, extract the identifiers from the row states, pass them into the AttachmentsClient (which would return URLs as an Effect), then generate a new set of row states with the resulting URLs. (The AttachmentClient shouldn't know about WidgetRowStates, so splicing them together would have to happen in the reducer.)

On the other hand, I'd like to reuse this functionality across other clients, such as a .live GizmoClient also using a local, no-sql database, etc. In addition, similar functionality would be useful as a client in environment itself, such as clearing the attachment cache, retrieving the total on disk storage, etc. That would imply having an AttachmentsClient in the environment that would be available to the reducer.

My line of thought at the moment is to use composition to pass some kind of attachment manager with shared functionality into clients, such as a WidgetClient, and also wrap that with a AttachmentClient struct that makes it available in the environment / reducer. However, I'm not sure if this too should be an TCA environment-like struct, a class that implements an AttachmentManager protocol, etc.? I can see the benefits of creating mock and live versions of this attachment manager as well.

1 Like

Personally I use few different solutions to share some functionality between environment clients.

  1. Create more specialized clients based on very general client.
    For example I have very general FileClient with endpoints like save, load, delete. I rarely use this client directly. Instead I create more specialized clients for specific tasks. For example: DocumentsFileClient, SettingsFileClient etc. which provided more detailed endpoints and uses general FileClient under the hood in live implementation. More specialized endpoints are like: deleteDocument(id:), deleteAllDocuments, loadArchivedDocuments etc. No protocols needed, just using concrete types. Naturally there are different flavors of this approach: totally separated types, nested types, generic wrappers etc.

  2. Create live implementation as a functions with some configuration as an argument.
    It's light version of previous approach. Instead of crating new clients types, use a function which will inject specialized configuration. Example: DatabaseClient.live(for: Database). Different features uses the same shared functionality, but accessing different databases.

  3. Upgrade Environment Client to full App Feature.
    Very often, when I need to compose/share functionality of environment client, it is just a sign that it has to be upgraded to app feature with separated state/action/environment/reducer. I really like when environment client (which are collections of side effect endpoints) are very simple and small. Because of that I can use all TCA composability/test/debug/ machinery for as much app logic as possible.

Thanks, Malauch.

I've currently using 2, but will eventually want to share some the work the .live client does with other clients specialized in the same way. (use the same underlying database, etc.) Wasn't sure if there were any advantages to making those effectively clients, in their own right, that I was missing.

Sounds like I could use 1 to share that functionality between clients.

Since these are similarly specialized, concrete types would work fine across all .live clients. .mock clients would use their own strategies and implementations, so trying to develop a generic, TCA style interface to share this functionality between .live and .mock clients doesn't seem to provide much of an advantage, and might even be counter productive.