Skip to content
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

feat: Cache configuration for a given ClientSession #959

Merged
merged 38 commits into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
9d11732
Added poc of config caching
NQuinn27 Jul 23, 2024
29d48dc
Extend network service to bubble up headers when wanted
NQuinn27 Jul 31, 2024
b29480d
Dont utilise headers in klarna configs
NQuinn27 Jul 31, 2024
daae31c
Added Swift-friendly Cache
NQuinn27 Jul 31, 2024
0e7114c
Refactored caching logic to dedicated class
NQuinn27 Jul 31, 2024
da839d8
use refactored cache
NQuinn27 Jul 31, 2024
f9d54db
Extract ttl from headers
NQuinn27 Aug 1, 2024
21a438e
Process images also for actions
NQuinn27 Aug 1, 2024
5d3c7e4
Upgrade config endpoints to 2.3
NQuinn27 Aug 1, 2024
f4e130e
Add config cache tests
NQuinn27 Aug 1, 2024
64f86ce
Fix tests for new interface
NQuinn27 Aug 1, 2024
661f8de
update fallback to 0
NQuinn27 Aug 1, 2024
25220ff
add tests for config module
NQuinn27 Aug 1, 2024
228c930
Add responses to mocks
NQuinn27 Aug 1, 2024
3558a8c
Formatting: Update ConfigurationCache.swift
NQuinn27 Aug 1, 2024
9787b5f
Formatting: Update PrimerAPIConfigurationModule.swift
NQuinn27 Aug 1, 2024
4b75f7b
Formatting: Update PrimerAPIConfigurationModule.swift
NQuinn27 Aug 1, 2024
cf0b551
Formatting: Update DefaultNetworkService.swift
NQuinn27 Aug 1, 2024
f9915e2
Formatting: Update NetworkService.swift
NQuinn27 Aug 1, 2024
06048cb
Merge branch 'master' into nq/cache_configuration
NQuinn27 Aug 1, 2024
6ff4a56
Clear cache after tests
NQuinn27 Aug 1, 2024
e0ad8b4
Update apiVersion in tests
NQuinn27 Aug 2, 2024
3ff3058
Added events for loading
NQuinn27 Aug 5, 2024
ca93d3e
Clear cache on cleanup
NQuinn27 Aug 5, 2024
5de40a6
Add checkout session active
NQuinn27 Aug 5, 2024
6c03ba0
Record load of vault manager
NQuinn27 Aug 5, 2024
b457de7
Merge branch 'master' into nq/cache_configuration
NQuinn27 Aug 5, 2024
5914d17
Record headless loading event
NQuinn27 Aug 5, 2024
5e887f9
Merge branch 'master' into nq/cache_configuration
NQuinn27 Aug 5, 2024
9e43a6f
fix: Validate PENDING in resume if showSuccessCheckoutOnPendingPaymen…
NQuinn27 Aug 6, 2024
4352203
Fix conflicts
NQuinn27 Aug 7, 2024
0d4e825
Release 2.28.0 (#963)
primer-security-integrations Aug 6, 2024
dcc5998
Add clientSessionCachingEnabled flag
NQuinn27 Aug 7, 2024
56c8a10
Add unit test to test for flag
NQuinn27 Aug 7, 2024
0e20c49
move cache clean to cleanup
NQuinn27 Aug 7, 2024
1df039a
Merge branch 'master' into nq/cache_configuration
NQuinn27 Aug 7, 2024
68e7282
Implement LogReporter
NQuinn27 Aug 7, 2024
9663e6d
Fix use cached config test
NQuinn27 Aug 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 56 additions & 3 deletions Sources/PrimerSDK/Classes/Core/Analytics/AnalyticsEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -537,19 +537,27 @@ struct TimerEventProperties: AnalyticsEventProperties {
var momentType: Analytics.Event.Property.TimerType
var id: String?
var params: [String: AnyCodable]?
var duration: TimeInterval?
var context: [String: Any]?

private enum CodingKeys: String, CodingKey {
case momentType
case id
case params
case duration
case context
}

fileprivate init(
momentType: Analytics.Event.Property.TimerType,
id: String?
id: String?,
duration: TimeInterval? = nil,
context: [String: Any]? = nil
) {
self.momentType = momentType
self.id = id
self.duration = duration
self.context = context

let sdkProperties = SDKProperties()
if let sdkPropertiesDict = try? sdkProperties.asDictionary(),
Expand All @@ -568,13 +576,17 @@ struct TimerEventProperties: AnalyticsEventProperties {
self.momentType = try container.decode(Analytics.Event.Property.TimerType.self, forKey: .momentType)
self.id = try container.decodeIfPresent(String.self, forKey: .id)
self.params = try container.decodeIfPresent([String: AnyCodable].self, forKey: .params)
self.duration = try container.decodeIfPresent(TimeInterval.self, forKey: .duration)
self.context = try container.decodeIfPresent([String: Any].self, forKey: .context)
}

func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(momentType, forKey: .momentType)
try container.encodeIfPresent(id, forKey: .id)
try container.encodeIfPresent(params, forKey: .params)
try container.encodeIfPresent(duration, forKey: .duration)
try container.encodeIfPresent(context, forKey: .context)
}
}

Expand Down Expand Up @@ -661,6 +673,7 @@ struct SDKProperties: Codable {
let sdkSettings: [String: AnyCodable]?
let sdkType: String?
let sdkVersion: String?
let context: [String: AnyCodable]?

private enum CodingKeys: String, CodingKey {
case clientToken
Expand All @@ -673,6 +686,7 @@ struct SDKProperties: Codable {
case sdkSettings
case sdkType
case sdkVersion
case context
}

fileprivate init() {
Expand All @@ -690,6 +704,7 @@ struct SDKProperties: Codable {

self.sdkType = Primer.shared.integrationOptions?.reactNativeVersion == nil ? "IOS_NATIVE" : "RN_IOS"
self.sdkVersion = VersionUtils.releaseVersionNumber
self.context = nil

if let settingsData = try? JSONEncoder().encode(PrimerSettings.current) {
let decoder = JSONDecoder()
Expand All @@ -714,6 +729,8 @@ struct SDKProperties: Codable {
self.sdkSettings = try container.decodeIfPresent([String: AnyCodable].self, forKey: .sdkSettings)
self.sdkType = try container.decodeIfPresent(String.self, forKey: .sdkType)
self.sdkVersion = try container.decodeIfPresent(String.self, forKey: .sdkVersion)
self.context = try container.decodeIfPresent([String: AnyCodable].self, forKey: .context)
Copy link
Contributor

@jnewc jnewc Aug 5, 2024

Choose a reason for hiding this comment

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

nit: We have a (spurious ..) extension to decode [String: Any] - could use that to keep this symmetric.


}

func encode(to encoder: Encoder) throws {
Expand All @@ -728,6 +745,7 @@ struct SDKProperties: Codable {
try container.encodeIfPresent(sdkSettings, forKey: .sdkSettings)
try container.encodeIfPresent(sdkType, forKey: .sdkType)
try container.encodeIfPresent(sdkVersion, forKey: .sdkVersion)
try container.encodeIfPresent(context, forKey: .context)
}
}

Expand Down Expand Up @@ -809,16 +827,51 @@ extension Analytics.Event {
}

static func timer(momentType: Property.TimerType,
id: String?) -> Self {
id: String?,
duration: TimeInterval? = nil,
context: [String: Any]? = nil) -> Self {
return .init(
eventType: .timerEvent,
properties: TimerEventProperties(
momentType: momentType,
id: id
id: id,
duration: duration,
context: context
)
)
}

enum DropInLoadingSource: String {
case universalCheckout = "UNIVERSAL_CHECKOUT"
case showPaymentMethod = "SHOW_PAYMENT_METHOD"
case vaultManager = "VAULT_MANAGER"
}

static func dropInLoading(duration: Int,
source: DropInLoadingSource) -> Self {
.timer(momentType: .end,
id: "DROP_IN_LOADING",
duration: TimeInterval(duration),
context: ["source": source.rawValue])
}

static func headlessLoading(duration: Int) -> Self {
.timer(momentType: .end, id: "HEADLESS_LOADING", duration: TimeInterval(duration))
}

enum ConfigurationLoadingSource: String {
case cache = "CACHE"
case network = "NETWORK"
}

static func configurationLoading(duration: Int,
source: ConfigurationLoadingSource) -> Self {
.timer(momentType: .end, id: "CONFIGURATION_LOADING",
duration: TimeInterval(duration),
context: ["source": source.rawValue])
}


static func allImagesLoading(momentType: Property.TimerType,
id: String?) -> Self {
return .init(
Expand Down
50 changes: 50 additions & 0 deletions Sources/PrimerSDK/Classes/Core/Cache/Cache.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//
// Cache.swift
// PrimerSDK
//
// Created by Niall Quinn on 31/07/24.
//

import Foundation

class Cache<Key: Hashable, Value> {
private let cache = NSCache<WrappedKey, Entry>()

func insert(_ value: Value, forKey key: Key) {
cache.setObject(Entry(value: value), forKey: WrappedKey(key))
}

func value(forKey key: Key) -> Value? {
guard let entry = cache.object(forKey: WrappedKey(key)) else {
return nil
}
return entry.value
}

func removeValue(forKey key: Key) {
cache.removeObject(forKey: WrappedKey(key))
}
}

private extension Cache {
final class WrappedKey: NSObject {
let key: Key
init(_ key: Key) { self.key = key }

override var hash: Int { return key.hashValue }

override func isEqual(_ object: Any?) -> Bool {
guard let value = object as? WrappedKey else {
return false
}
return value.key == key
}
}
}

private extension Cache {
final class Entry {
let value: Value
init(value: Value) { self.value = value }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
//
// ConfigurationCache.swift
// PrimerSDK
//
// Created by Niall Quinn on 31/07/24.
//

import Foundation

protocol ConfigurationCaching {
func clearCache()
func setData(_ data: ConfigurationCachedData, forKey key: String)
func data(forKey key: String) -> ConfigurationCachedData?
}

class ConfigurationCache: ConfigurationCaching {
static let shared = ConfigurationCache()
private var cache = Cache<String, ConfigurationCachedData>()

func clearCache() {
cache = Cache<String, ConfigurationCachedData>()
}

func data(forKey key: String) -> ConfigurationCachedData? {
guard cachingEnabled else {
return nil
}
if let cachedData = cache.value(forKey: key) {
if validateCachedConfig(key: key, cachedData: cachedData) == false {
cache.removeValue(forKey: key)
return nil
}
return cachedData
}
return nil
}

func setData(_ data: ConfigurationCachedData, forKey key: String) {
guard cachingEnabled else {
return
}
// Cache includes at most one cached configuration
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a potential case for having a SingleItemCache wrapper around NSCache instead? Then the key could be passed in the init and removed from all the upstream methods here

My concern would be someone seeing this and thinking that it supports multiple configurations because of the provision of a key

clearCache()
cache.insert(data, forKey: key)
}

private func validateCachedConfig(key: String, cachedData: ConfigurationCachedData) -> Bool {
let timestamp = cachedData.timestamp
let now = Date().timeIntervalSince1970
let timeInterval = now - timestamp

if timeInterval > cachedData.ttl {
return false
}

return true
}

private var cachingEnabled: Bool {
PrimerSettings.current.clientSessionCachingEnabled
}
}

class ConfigurationCachedData {

let config: PrimerAPIConfiguration
let timestamp: TimeInterval
let ttl: TimeInterval

init(config: PrimerAPIConfiguration, headers: [String: String]? = nil) {
// Extract ttl from headers
self.config = config
self.timestamp = Date().timeIntervalSince1970
self.ttl = Self.extractTtlFromHeaders(headers)
}

static let FallbackCacheExpiration: TimeInterval = 0
static let CacheHeaderKey = "x-primer-session-cache-ttl"

private static func extractTtlFromHeaders(_ headers: [String: String]?) -> TimeInterval {
guard let headers,
let ttlHeaderValue = headers[Self.CacheHeaderKey],
let ttlInt = Int(ttlHeaderValue) else {
return Self.FallbackCacheExpiration
}
return TimeInterval(ttlInt)
}
}
Loading
Loading