Skip to content

Conversation

@aashishpatil-g
Copy link
Collaborator

Implement caching in base (core) SDK

For googlers, refer to the doc go/fdc-sdk-caching

@dconeybe
Copy link
Collaborator

/gemini review

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
struct QueryRequest<Variable: OperationVariable>: OperationRequest, Hashable, Equatable {
private(set) var operationName: String
private(set) var variables: Variable?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is minor, but I would think the variables would map to an array of Variable (e.g. [Variable])?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Variable is typically a container struct that contains all the variables for that operation.

The Gen SDK creates schema specific Variable container structs that meet OperationVariable protocol requirements (Codable, Equatable, Hashable)

Example -
https://github.com/firebase/data-connect-ios-sdk/blob/main/Tests/Integration/Gen/KitchenSink/Sources/KitchenSinkOperations.swift#L161

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The container struct could also contain an array for example.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fields of the container struct map to the names of the variables of that operation.

public private(set) var lastError: DataConnectError?

/// Source of the query results (server, local cache, ...)
public private(set) var source: DataSource?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this mutable again? Is it because the contents of this class will change each time an update is observed?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are observed properties that are updated each time results are updated.

Its a class so we end up updating the same instance of the Observed class.


private let cache: Cache?

private var ttl: TimeInterval? = 10.0 //
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like a comment was intended? Also, does this need to be optional?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll fix this now that we have the ttl field finalized. This might need some refactoring.

Comment on lines +31 to +32
case hydrated // JSON data is full hydrated and contains full data in the tree
case dehydrated // JSON data is dehydrated and only contains refs to actual data objects
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These docs are sort of self-referencing. What does this concept of (de)hydration mean?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its explained in the go/fdc-sdk-caching internal doc (Design tab).

// QueryRef requesting impacted
let requestor: (any QueryRefInternal)?

init(requestor: (any QueryRefInternal)? = nil) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would we want to accept nil here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ResultTreeProcessor isn't required to have a requestor and can function without it too. I kept it optional to maintain the decoupling.

Go down the tree and convert them to entity nodes
For each Node
- extract globalID
- Get the EDO for the globalID
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EDO?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EntityDataObject - I need to improve that comment but details are in the go/fdc-sdk-caching doc (Design tab)

- Get the EDO for the globalID
- extract scalars and update EDO with scalars
- for each array
- recursively process each object (could be scalar or composite)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the recursion being done by Apple's coders? I'm not noticing the recursive calls in below implementations.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I am hooking into the codable process here.

}

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
class SQLiteCacheProvider: CacheProvider {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All optional suggestions/questions

  1. Maybe worth generating some unit tests for this class?
  2. Check whether db storage location is in a location where it will be backed up or not and whether that's either is desired.
  3. Can/should this be an actor instead? I recall in FirebaseAuth we ran into some issues when mixing async/await with dispatch queues, mainly because queue.sync is blocking. I still have some doubts but generally I'm of the belief that only one cocnucrrency system should be used within an SDK (GCD or async/await).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haven't done unit tests - will do these in a later PR. I've tested this functionally for now.

Db storage is in the Documents folder

  • Cache gets backed up
  • Cache gets deleted when app is deleted
  • (I think) if device encryption is enabled, data stays encrypted.

Reg sync ->
You are right about not mixing systems but async but its difficult to use async everywhere here.

Actor or async/await require calls from an async context. The dehydration process calls the cache provider from within the coding process which is sync (encode / init(decoder)) are sync.

The outer cache API is async so we aren't blocking the UI. The queue.sync is selectively used here to protect the access to the db resource.

The way it is used here is essentially converting this class into an actor using the following rules -

  • internal dispatch queue is serial
  • only public func (entry points) initiate a sync call
  • private (internal) func never initiate a sync call but will always check to ensure they are called on queue.
  • one public function never calls another public func (avoid deadlock).

@ncooke3
Copy link
Member

ncooke3 commented Nov 18, 2025

@aashishpatil-g, left a first pass!

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.

4 participants