-
Notifications
You must be signed in to change notification settings - Fork 1
Cache implementation #68
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
TODO - needs cleanup
|
/gemini review |
|
/gemini review |
There was a problem hiding this 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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
| /// 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> |
There was a problem hiding this comment.
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:
| 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 { |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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:
| XCTAssertTrue(user1 == user2) | |
| XCTAssertTrue(user1 === user2) |
|
|
||
| import Foundation | ||
|
|
||
| import FirebaseCore |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.variablesThere was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
optional nit:
| // 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? |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This import looks unused.
| import CryptoKit |
| import CryptoKit | ||
| import Foundation | ||
|
|
||
| import Firebase |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This import looks unused.
| 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, *) |
There was a problem hiding this comment.
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? |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: looks unused
| import CryptoKit |
| import CryptoKit | ||
| import Foundation | ||
|
|
||
| import Firebase |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: looks unused
| import Firebase |
|
|
||
| private let cache: Cache? | ||
|
|
||
| private var ttl: TimeInterval? = 10.0 // |
There was a problem hiding this comment.
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, *) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why the macOS bump?
https://developer.apple.com/documentation/observation/observable
|
|
||
| operationsManager = OperationsManager(grpcClient: grpcClient) | ||
| if let cache { | ||
| self.cache = Cache(config: cache.config, dataConnect: self) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: unused import
| 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. |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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?
Implement caching in base (core) SDK
For googlers, refer to the doc go/fdc-sdk-caching