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

@aashishpatil-g
Copy link
Collaborator Author

/gemini review

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a comprehensive caching layer to the SDK, a significant and valuable feature. The implementation includes support for both in-memory and persistent storage via SQLite, data normalization to reduce redundancy, and automatic cache invalidation on authentication state changes. The overall architecture is well-thought-out. However, the review has identified several critical and high-severity issues that must be addressed. These include SQL injection vulnerabilities in the SQLite provider, multiple potential crash scenarios due to force-unwrapping optionals, and a critical performance issue from a hardcoded sleep. There are also other bugs and areas for improvement in robustness and code cleanliness. Please address these points to ensure the stability, security, and performance of the new caching feature.

*/
guard !errorInfoList.isEmpty else {
// TODO: Extract ttl, server timestamp
return ServerResponse(jsonResults: resultsString, ttl: 10.0)

Choose a reason for hiding this comment

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

medium

The ttl (Time To Live) for the server response is hardcoded to 10.0. The TODO comment indicates this is temporary. This value should be extracted from the server response or the service configuration to allow for flexible and correct caching behavior, rather than being a fixed constant.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This will be resolved once we finalize on ttl location


import Foundation

/// Struct returned by operation calls - query or mutation
Copy link
Member

Choose a reason for hiding this comment

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

I've read in Apple's docs that they will refer to structs in doc comments as "structure" or "value".

What about something like this?

Suggested change
/// Struct returned by operation calls - query or mutation
/// The value returned by query or mutation operations.


public static func == (lhs: MutationRef<ResultData, Variable>,
rhs: MutationRef<ResultData, Variable>) -> Bool {
return lhs.request as? MutationRequest<Variable> == rhs.request as? MutationRequest<Variable>
Copy link
Member

Choose a reason for hiding this comment

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

There is one possible edge case where if two MutationRefs with different, non-MutationRequest-typed requests are compared, the comparison will be true.

Would it make sense to change the request property's type from any OperationRequest to MutationRequest<Variable>?

public class MutationRef<
  ResultData: Decodable & Sendable,
  Variable: OperationVariable
>: OperationRef {
  private var request: MutationRequest<Variable> // ✨

Doing so would clean up the need for optional downcasting:

Suggested change
return lhs.request as? MutationRequest<Variable> == rhs.request as? MutationRequest<Variable>
return lhs.request == rhs.request

I think the suggestion is worth it if a MutationRef maps 1:1 with a MutationRequest.

// Confirm that providing same entity cache id uses the same EntityDataObject instance
func testEntityDataObjectReuse() throws {
do {
guard let cacheProvider else {
Copy link
Member

Choose a reason for hiding this comment

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

optional nit: The guard here and in the other test case could be simplified with try XCTUnwrap(cacheProvider) or making the proerty implicitly non-optional with a !.

let user2 = cacheProvider.entityData(reused_id)

// both user objects should be references to same instance
XCTAssertTrue(user1 == user2)
Copy link
Member

Choose a reason for hiding this comment

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

Given above comment, I think this should be:

Suggested change
XCTAssertTrue(user1 == user2)
XCTAssertTrue(user1 === user2)


import Foundation

import FirebaseCore
Copy link
Member

Choose a reason for hiding this comment

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

Is import needed anymore, given the code removal?

return false
}

if lhs.variables == nil && rhs.variables == nil {
Copy link
Member

Choose a reason for hiding this comment

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

nil == nil == true so couldn't the here and below be replaced with:

    return lhs.variables == rhs.variables

Copy link
Member

Choose a reason for hiding this comment

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

couldn't the here

self.variables = variables
}

// Hashable and Equatable implementation
Copy link
Member

Choose a reason for hiding this comment

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

optional nit:

Suggested change
// Hashable and Equatable implementation
// MARK: - Hashable and Equatable

@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])?

import CryptoKit
import Foundation

import Firebase
Copy link
Member

Choose a reason for hiding this comment

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

This import looks unused.

// See the License for the specific language governing permissions and
// limitations under the License.

import CryptoKit
Copy link
Member

Choose a reason for hiding this comment

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

This import looks unused.

Suggested change
import CryptoKit

import CryptoKit
import Foundation

import Firebase
Copy link
Member

Choose a reason for hiding this comment

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

This import looks unused.

Suggested change
import Firebase

/// - ``lastError``: Published variable that contains ``DataConnectError`` if last fetch had error.
/// If last fetch was successful, this variable is cleared
@available(macOS 14, iOS 17, tvOS 17, watchOS 10, *)
@available(macOS 15, iOS 17, tvOS 17, watchOS 10, *)
Copy link
Member

Choose a reason for hiding this comment

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

Why the macOS version bump? The Observable macro looks to be available on macOS 14?

https://developer.apple.com/documentation/observation/observable

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?

// See the License for the specific language governing permissions and
// limitations under the License.

import CryptoKit
Copy link
Member

Choose a reason for hiding this comment

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

nit: looks unused

Suggested change
import CryptoKit

import CryptoKit
import Foundation

import Firebase
Copy link
Member

Choose a reason for hiding this comment

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

nit: looks unused

Suggested change
import Firebase


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?


if publisher == .auto || publisher == .observableMacro {
if #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) {
if #available(iOS 17, macOS 15, tvOS 17, watchOS 10, *) {
Copy link
Member

Choose a reason for hiding this comment

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


operationsManager = OperationsManager(grpcClient: grpcClient)
if let cache {
self.cache = Cache(config: cache.config, dataConnect: self)
Copy link
Member

Choose a reason for hiding this comment

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

Won't there be a strong retain cycle between Cache and DataConnect?

Copy link
Member

Choose a reason for hiding this comment

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

Maybe Cache only needs to retain immutable data from the DC instance. Or the Cache weakly retains DC...

public let storage: Storage // default provider is persistent type
public let maxSizeBytes: UInt64

public init(storage: Storage = .persistent, maxSize: UInt64 = 100_000_000) {
Copy link
Member

Choose a reason for hiding this comment

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

suggestion: Maybe generate a doc comment for this method.


import Foundation

import FirebaseCore
Copy link
Member

Choose a reason for hiding this comment

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

nit: unused import

Suggested change
import FirebaseCore


@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
struct ResultTree {
let data: String // tree data - could be hydrated or dehydrated.
Copy link
Member

Choose a reason for hiding this comment

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

optional nit: generally, i would put comments above the lines so they are more discoverable

}
}

extension ResultTree: Codable {
Copy link
Member

Choose a reason for hiding this comment

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

What is custom here? Why not use the generated Codable impl?

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