-
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
Open
aashishpatil-g
wants to merge
33
commits into
main
Choose a base branch
from
ap/coreSdkCacheV1
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+2,207
−262
Open
Changes from all commits
Commits
Show all changes
33 commits
Select commit
Hold shift + click to select a range
7c92921
Initial Stub Classes
aashishpatil-g f56a8cc
Wire up Initialization of CacheProvider
aashishpatil-g f655a15
query level caching
aashishpatil-g 90c7ae1
Cache Normalization Implementation
aashishpatil-g 5174e03
Persistent Provider
aashishpatil-g ad71e3c
Add last_accessed field
aashishpatil-g c0a024b
Uber Cache object, support Auth uid scope and Refactor classes
aashishpatil-g 0bcdf09
Implement operationId on QueryRef
aashishpatil-g ad749e7
Fix sha256 formatter.
aashishpatil-g 35dc280
Accumulate Impacted Refs and Reload Local
aashishpatil-g 6cd60ef
Update construction of cacheIdentifier to include connector and locat…
aashishpatil-g 824a41e
Use inmemory SQLite for Ephemeral cache provider
aashishpatil-g f503f5e
Minor updates to API
aashishpatil-g e57bbb4
Refactor BackingDataObject, STubDataObject names to EntityDataObject …
aashishpatil-g 353caf5
DispatchQueue for EntityDataObject access
aashishpatil-g 78599a3
Externalize table and column name strings into constants
aashishpatil-g 250e905
API Review feedback
aashishpatil-g f1aa717
Code formatting updates
aashishpatil-g d9b4807
Refactor name of OperationResultSource
aashishpatil-g 1c666ec
API feedback - rename maxSize => maxSizeBytes
aashishpatil-g 8ea6762
API Council Review Feedback
aashishpatil-g a807d9f
Move ServerResponse
aashishpatil-g 336e41b
Update cache type to match API review feedback
aashishpatil-g 37493c4
Cleanup and minor fixes to docs
aashishpatil-g dd8569c
Update globalID value
aashishpatil-g fad7e2f
Fix format
aashishpatil-g a62c16b
Fix copyright notices
aashishpatil-g 30940a6
Fix @available
aashishpatil-g 5482987
fix integration tests
aashishpatil-g 1a9aeb9
Gemini Review part 1
aashishpatil-g 12da757
formatting fixes
aashishpatil-g 334e710
Convert sync queue calls to actor
aashishpatil-g b1569e8
comments
aashishpatil-g File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,159 @@ | ||
| // Copyright 2025 Google LLC | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| import FirebaseAuth | ||
|
|
||
| // FDC field name in server response that identifies a GlobalID | ||
| let GlobalIDKey: String = "_id" | ||
|
|
||
| // Client cache that internally uses a CacheProvider to store content. | ||
| @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) | ||
| actor Cache { | ||
| let config: CacheSettings | ||
| let dataConnect: DataConnect | ||
|
|
||
| private var cacheProvider: CacheProvider? | ||
|
|
||
| // holding it to avoid dealloc | ||
| private var authChangeListenerProtocol: NSObjectProtocol? | ||
|
|
||
| init(config: CacheSettings, dataConnect: DataConnect) { | ||
| self.config = config | ||
| self.dataConnect = dataConnect | ||
|
|
||
| // this is a potential race since update or get could get scheduled before initialize | ||
| // workarounds are complex since caller DataConnect APIs aren't async | ||
| Task { | ||
| await initializeCacheProvider() | ||
| await setupChangeListeners() | ||
| } | ||
| } | ||
|
|
||
| private func initializeCacheProvider() { | ||
| let identifier = contructCacheIdentifier() | ||
|
|
||
| // Create a cacheProvider if - | ||
| // we don't have an existing cacheProvider | ||
| // we have one but its identifier is different than new one (e.g. auth uid changed) | ||
| if cacheProvider != nil, cacheProvider?.cacheIdentifier == identifier { | ||
| return | ||
| } | ||
|
|
||
| do { | ||
| switch config.storage { | ||
| case .memory: | ||
| cacheProvider = try SQLiteCacheProvider(identifier, ephemeral: true) | ||
| case .persistent: | ||
| cacheProvider = try SQLiteCacheProvider(identifier, ephemeral: false) | ||
| } | ||
| } catch { | ||
| DataConnectLogger.error("Unable to initialize Persistent provider \(error)") | ||
| } | ||
| } | ||
|
|
||
| private func setupChangeListeners() { | ||
| authChangeListenerProtocol = Auth.auth(app: dataConnect.app).addStateDidChangeListener { _, _ in | ||
| self.initializeCacheProvider() | ||
| } | ||
| } | ||
|
|
||
| // Create an identifier for the cache that the Provider will use for cache scoping | ||
| private func contructCacheIdentifier() -> String { | ||
| let identifier = | ||
| "\(config.storage)-\(dataConnect.app.options.projectID!)-\(dataConnect.app.name)-\(dataConnect.connectorConfig.serviceId)-\(dataConnect.connectorConfig.connector)-\(dataConnect.connectorConfig.location)-\(Auth.auth(app: dataConnect.app).currentUser?.uid ?? "anon")-\(dataConnect.settings.host)" | ||
| let encoded = identifier.sha256 | ||
| DataConnectLogger.debug("Created Encoded Cache Identifier \(encoded) for \(identifier)") | ||
| return encoded | ||
| } | ||
|
|
||
| func resultTree(queryId: String) -> ResultTree? { | ||
| // result trees are stored dehydrated in the cache | ||
| // retrieve cache, hydrate it and then return | ||
| guard let cacheProvider else { | ||
| DataConnectLogger.error("CacheProvider is nil in the Cache") | ||
| return nil | ||
| } | ||
|
|
||
| guard let dehydratedTree = cacheProvider.resultTree(queryId: queryId) else { | ||
| return nil | ||
| } | ||
|
|
||
| do { | ||
| let resultsProcessor = ResultTreeProcessor() | ||
| let (hydratedResults, rootObj) = try resultsProcessor.hydrateResults( | ||
| dehydratedTree.data, | ||
| cacheProvider: cacheProvider | ||
| ) | ||
|
|
||
| let hydratedTree = ResultTree( | ||
| data: hydratedResults, | ||
| ttl: dehydratedTree.ttl, | ||
| cachedAt: dehydratedTree.cachedAt, | ||
| lastAccessed: dehydratedTree.lastAccessed, | ||
| rootObject: rootObj | ||
| ) | ||
|
|
||
| return hydratedTree | ||
| } catch { | ||
| DataConnectLogger.warning("Error decoding result tree \(error)") | ||
| return nil | ||
| } | ||
| } | ||
|
|
||
| func update(queryId: String, response: ServerResponse, requestor: (any QueryRefInternal)? = nil) { | ||
| // server response contains hydrated trees | ||
| // dehydrate (normalize) the results and store dehydrated trees | ||
| guard let cacheProvider = cacheProvider else { | ||
| DataConnectLogger | ||
| .debug("Cache provider not initialized yet. Skipping update for \(queryId)") | ||
| return | ||
| } | ||
| do { | ||
| let processor = ResultTreeProcessor() | ||
| let (dehydratedResults, rootObj, impactedRefs) = try processor.dehydrateResults( | ||
| response.jsonResults, | ||
| cacheProvider: cacheProvider, | ||
| requestor: requestor | ||
| ) | ||
|
|
||
| cacheProvider | ||
| .setResultTree( | ||
| queryId: queryId, | ||
| tree: .init( | ||
| data: dehydratedResults, | ||
| ttl: response.ttl, | ||
| cachedAt: Date(), | ||
| lastAccessed: Date(), | ||
| rootObject: rootObj | ||
| ) | ||
| ) | ||
|
|
||
| for refId in impactedRefs { | ||
| guard let q = dataConnect.queryRef(for: refId) as? (any QueryRefInternal) else { | ||
| continue | ||
| } | ||
| Task { | ||
| do { | ||
| try await q.publishCacheResultsToSubscribers(allowStale: true) | ||
| } catch { | ||
| DataConnectLogger | ||
| .warning("Error republishing cached results for impacted queryrefs \(error))") | ||
| } | ||
| } | ||
| } | ||
| } catch { | ||
| DataConnectLogger.warning("Error updating cache for \(queryId): \(error)") | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| // Copyright 2025 Google LLC | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| import Foundation | ||
|
|
||
| import FirebaseCore | ||
|
|
||
| // Key to store cache provider in Codables userInfo object. | ||
| let CacheProviderUserInfoKey = CodingUserInfoKey(rawValue: "fdc_cache_provider")! | ||
|
|
||
| @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) | ||
| protocol CacheProvider { | ||
| var cacheIdentifier: String { get } | ||
|
|
||
| func resultTree(queryId: String) -> ResultTree? | ||
| func setResultTree(queryId: String, tree: ResultTree) | ||
|
|
||
| func entityData(_ entityGuid: String) -> EntityDataObject | ||
| func updateEntityData(_ object: EntityDataObject) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| // Copyright 2025 Google LLC | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| /// Specifies the cache configuration for a Firebase Data Connect instance | ||
| @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) | ||
| public struct CacheSettings: Sendable { | ||
| public enum Storage: Sendable { | ||
| case persistent | ||
| case memory | ||
| } | ||
|
|
||
| public let storage: Storage // default provider is persistent type | ||
| public let maxSizeBytes: UInt64 | ||
|
|
||
| public init(storage: Storage = .persistent, maxSize: UInt64 = 100_000_000) { | ||
| self.storage = storage | ||
| maxSizeBytes = maxSize | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| // Copyright 2025 Google LLC | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| // Used for inline inline hydration of entity values | ||
| @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) | ||
| struct DynamicCodingKey: CodingKey { | ||
| var intValue: Int? | ||
| let stringValue: String | ||
| init?(intValue: Int) { return nil } | ||
| init?(stringValue: String) { self.stringValue = stringValue } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.