-
Notifications
You must be signed in to change notification settings - Fork 13
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
Update TCA to latest version #228
base: master
Are you sure you want to change the base?
Conversation
d94679a
to
47015e3
Compare
@@ -26,9 +26,9 @@ jobs: | |||
- name: Select Xcode ${{ matrix.xcode }} | |||
run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app | |||
- name: Run ${{ matrix.config }} tests | |||
run: make CONFIG=${{ matrix.config }} test-library | |||
run: make XCODE=${{ matrix.xcode }} CONFIG=${{ matrix.config }} test-library |
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.
Test results need to be unique.
@@ -1,11 +1,12 @@ | |||
// swift-tools-version: 5.6 | |||
// swift-tools-version:5.9 |
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.
We need two different version of the package to support older versions of swift.
Package.swift
Outdated
import PackageDescription | ||
|
||
let package = Package( | ||
name: "klaviyo-swift-sdk", | ||
platforms: [.iOS(.v13)], | ||
platforms: [.iOS(.v15), .macOS(.v10_15)], |
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.
Might not want macOS but also maybe it will just work.
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.
Have we ever tested / know about anyone using the klaviyo sdk in a mac os app?
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.
So people have run the ios app on a mac but I'm not sure if that's the same thing.
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 ended up taking this out. We need to do a little more work to support this.
path: "Sources/KlaviyoSwiftExtension"), | ||
|
||
// Vendorized Things | ||
.target( |
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.
These will all be combined into one thing soon.
@@ -62,7 +59,10 @@ public struct KlaviyoSDK { | |||
/// - Returns: a KlaviyoSDK instance | |||
@discardableResult | |||
public func initialize(with apiKey: String) -> KlaviyoSDK { | |||
dispatchOnMainThread(action: .initialize(apiKey)) | |||
Task { | |||
let appContextInfo = await environment.appContextInfo() |
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.
Retreiving app context requires mainactor access so this can no longer be done inside the reducer. It is now done here and passed through.
@@ -30,28 +31,28 @@ enum RetryInfo: Equatable { | |||
enum KlaviyoAction: Equatable { | |||
/// Sets the API key to state. If the state is already initialized then the push token is moved over to the company with the API key provided in this action. | |||
/// Loads the state from disk and carries over existing items from the queue. This emits `completeInitialization` at the end with the state loaded from disk. | |||
case initialize(String) | |||
case initialize(String, AppContextInfo) |
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.
Most actions now include app context now.
if action.requiresInitialization, | ||
case .uninitialized = state.initalizationState { | ||
environment.emitDeveloperWarning("SDK must be initialized before usage.") | ||
environment.logger.error("SDK must be initialized before usage.") |
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 requires access to mainactor and cannot be async currently so it has been switch to a logger error instead. This is done in many places.
await send(.setExternalId(externalId)) | ||
case let .setPhoneNumber(phoneNumber): | ||
await send(.setPhoneNumber(phoneNumber)) | ||
case let .event(event, appContextInfo): |
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.
Pending actions also need app context.
.merge(with: environment.appLifeCycle.lifeCycleEvents().map(\.transformToKlaviyoAction).eraseToEffect()) | ||
.merge(with: klaviyoSwiftEnvironment.stateChangePublisher().eraseToEffect()) | ||
.merge(with: .run { send in | ||
for await action in await klaviyoSwiftEnvironment.lifeCyclePublisher() { |
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.
changed to async implementation from combine.
await send(.cancelInFlightRequests) | ||
})) | ||
|
||
return Effect.cancel(id: CancelIds.request) |
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.
Cancellations work differently in the newer TCA.
@@ -320,7 +333,7 @@ struct KlaviyoReducer: ReducerProtocol { | |||
state.flushing = false | |||
return .none | |||
} | |||
return .task { .sendRequest }.cancellable(id: RequestId.self) | |||
return .send(.sendRequest).cancellable(id: CancelIds.request) |
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.
.task
is now .send
.
@@ -5,7 +5,7 @@ | |||
// Created by Ajay Subramanya on 6/23/23. | |||
// | |||
import Foundation | |||
import UserNotifications | |||
@preconcurrency import UserNotifications |
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.
might want a pragma here...
@@ -80,7 +80,8 @@ class KlaviyoWebViewController: UIViewController, WKUIDelegate { | |||
Task { [weak self] in | |||
guard let self else { return } | |||
|
|||
for await (script, callback) in self.viewModel.scriptStream { | |||
let scriptStream = self.viewModel.scriptStream |
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.
Caught by swift strict concurrency mode.
private var continuation: AsyncStream<(script: String, callback: ((Result<Any?, Error>) -> Void)?)>.Continuation? | ||
lazy var scriptStream: AsyncStream<(script: String, callback: ((Result<Any?, Error>) -> Void)?)> = AsyncStream { [weak self] continuation in | ||
private var continuation: AsyncStream < (script: String, callback: (@Sendable (Result<Any?, Error>) -> Void)?)>.Continuation? | ||
lazy var scriptStream: AsyncStream < (script: String, callback: (@Sendable (Result<Any?, Error>) -> Void)?)> = AsyncStream { [weak self] continuation in |
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.
Needs to be Sendable.
Sources/KlaviyoSwift/Klaviyo.swift
Outdated
@@ -27,10 +24,9 @@ func dispatchOnMainThread(action: KlaviyoAction) { | |||
/// ``` | |||
/// | |||
/// From there you can you can call the additional methods below to track events and profile. | |||
@MainActor |
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 probably one of the more significant changes in this PR.
97e9526
to
c617eb9
Compare
💯 great idea! |
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.
with this and all other imported files, can we re-indent with 4 spaces instead of 2?
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 it better to import as they so they can be diffed easily?
Sources/KlaviyoSDKDependencies/ComposableArchitecture/CaseReducer.swift
Outdated
Show resolved
Hide resolved
aee9f46
to
1c3b070
Compare
s.homepage = "https://github.com/klaviyo/klaviyo-swift-sdk" | ||
s.license = { :type => "MIT", :file => "LICENSE" } | ||
s.author = { "Mobile @ Klaviyo" => "[email protected]" } | ||
s.source = { :git => "https://github.com/klaviyo/klaviyo-swift-sdk.git", :tag => s.version.to_s } |
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'm probably not looking in the right file, but I am curious what dependencies are going in this. I saw in a lower file that both core and this were imported, why is that? Is this where the dependencies of our SDK live?
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.
We shouldn't have any dependencies after this change in the sdk properly. I've internned pretty much everything except in tests which uses combine schedulers and a few other misc things.
private let _defaultAppContextInfo: AppContextInfo? = nil | ||
|
||
@MainActor | ||
public func getDefaultAppContextInfo() -> AppContextInfo { |
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.
Just want to ask about this - was this change something necessary with the TCA update, or was this just a simple enough change with the refactor? Seems like a decent sized change, going from a getter computed value rather than a struct with static values. I will admit though I am confused how something can be static but also computed at runtime with the ternary operator (although maybe its more compile time since we'll know about those info values then)
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.
Honestly I struggled with this part a lot because we had inconsistent access to this data in the sdk and some of it required main thread access. With these changes now we can at least guarantee we always access this info on the main thread.
@@ -7,7 +7,7 @@ | |||
|
|||
import UIKit | |||
|
|||
public enum PushEnablement: String, Codable { | |||
public enum PushEnablement: String, Codable, Sendable { |
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.
Does adding all of these imply that we didn't have concurrency protection in previous versions? Or was this also necessary to add with the TCA updates.
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 think actually other than the bug that was caught by an outside developer we were being pretty careful with concurrency. For a lot of these changes we are just making the compiler happy but hopefully it will stop us from making future concurrency mistakes.
|
||
extension AnyCodable: _AnyEncodable, _AnyDecodable {} | ||
|
||
extension AnyCodable: Equatable { |
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.
very suprised this isn't natively supported in swift. Is this something that we do get with structs but not necessarily codables? Makes sense that we need it for state management operations but I'm just surprised to see it so explicit. Does this mean we need to manually add any new types supported from the backend?
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.
See my later comment.
} | ||
|
||
#if canImport(Foundation) | ||
private func encode(nsnumber: NSNumber, into container: inout SingleValueEncodingContainer) throws { |
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 whole function is wild to me
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 part of a dependency called AnyCodable. We encode some json with arbitrary key/value pairs. Anycodable helps this play nice with swift codable.
Definitely this is something exacerbated by my lack of ios domain knowledge, but I'm wondering if it could be helpful to point out some of the main files / directories we should be checking out for reviewing. I'm guessing all the 'sendable added' files are probably pretty arbitrary, but are there others that are more or less copy/paste code that are not super important for review? Going to still try reviewing incrementally but feeling a smidge overwhelmed by the content still |
@dan-peluso I put a header on any file that was taken from tca or other repos. That should be a good indicator it was copied verbatim. The header has information on whether the content was modified and what changed. Credit to @ab1470 for the idea! |
1c3b070
to
d678cf5
Compare
* Update TCA to latest * incorporate Anycodable * fix many concurrency issues
75c7aaf
to
63c877e
Compare
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.
still reviewing the overall PR but here are some minor suggestions to work on while I continue reviewing
Sources/KlaviyoSDKDependencies/IssueReporting/IssueReporters/RuntimeWarningReporter.swift
Outdated
Show resolved
Hide resolved
Tests/KlaviyoSwiftTests/Vendor/ComposableArchitecture/KeyPath+Sendable.swift
Show resolved
Hide resolved
Tests/KlaviyoSwiftTests/Vendor/ComposableArchitecture/DispatchQueue.swift
Show resolved
Hide resolved
Tests/KlaviyoSwiftTests/Vendor/CustomDump/Conformances/CoreLocation.swift
Show resolved
Hide resolved
Tests/KlaviyoSwiftTests/Vendor/IdentifiedCollections/Collections.swift
Outdated
Show resolved
Hide resolved
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.
😮💨 big one. Left a bunch of comments but mostly minor stuff. I'll look into the KlaviyoWebView
/ViewModel
tomorrow to see what I can do to fix it.
@@ -25,8 +25,6 @@ extension IssueReporter where Self == _RuntimeWarningReporter { | |||
/// fault-level messages to the console. | |||
/// | |||
/// Use ``IssueReporter/runtimeWarning`` to create one of these values. | |||
/// Copied verbatim from xctest dynamic overlay v1.3.0 on 11/15/2024 | |||
/// https://github.com/pointfreeco/xctest-dynamic-overlay/tree/1.3.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.
Nit: remove a space to make sure that inline documentation works properly
} | ||
} | ||
} | ||
}, appContextInfo: { |
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.
}, appContextInfo: { | |
}, | |
appContextInfo: { |
nit, aligning parameter names
} | ||
|
||
public enum KlaviyoDecodingError: Error { | ||
case invalidType | ||
case invalidJson | ||
} | ||
|
||
public struct DataDecoder { | ||
public struct DataDecoder: @unchecked Sendable { |
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 tried removing the @unchecked
to see what would happen, and I didn't see any compiler errors or warnings. Are you sure we need it?
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.
note, I was testing on Xcode 16.2. Maybe there are warnings/errors on earlier versions?
fileprivate func appendMetadataToProperties(pushToken: String?) -> [String: Any]? { | ||
let context = environment.appContextInfo() | ||
let metadata: [String: Any] = [ | ||
func appendMetadataToProperties(context: AppContextInfo, pushToken: String?) -> [String: Any]? { |
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.
func appendMetadataToProperties(context: AppContextInfo, pushToken: String?) -> [String: Any]? { | |
fileprivate func appendMetadataToProperties(context: AppContextInfo, pushToken: String?) -> [String: Any]? { |
Was there a reason for removing the fileprivate
? I tried re-adding it and it seems fine. No compiler issues
stopTimer() | ||
|
||
// Create a new DispatchSourceTimer and start it | ||
let newTimer = DispatchSource.makeTimerSource(queue: .global()) |
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.
let newTimer = DispatchSource.makeTimerSource(queue: .global()) | |
let timerQueue = DispatchQueue(label: "com.klaviyo.TimerActor.timerQueue") | |
let newTimer = DispatchSource.makeTimerSource(queue: timerQueue) | |
ChatGPT made this suggestion, so big grain of salt here. Its explanation: "Currently, the DispatchSourceTimer is created on a global queue. While functional, this approach does not take advantage of the actor’s natural thread confinement, which ensures thread safety. You can improve it by creating the timer on a custom serial queue for better control."
I searched Github and it seems like a lot of people create timers on the global queue or the main queue, but some create a custom queue. I'm not sure what the right answer is, maybe it doesn't really matter? What do you think?
}, | ||
getBackgroundSetting: { | ||
.create(from: UIApplication.shared.backgroundRefreshStatus) | ||
}, networkSession: { createNetworkSession() }, |
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.
}, networkSession: { createNetworkSession() }, | |
}, | |
networkSession: { createNetworkSession() }, |
nit, aligning all parameters
extension LoggerClient { | ||
static var lastLoggedMessage: String? | ||
static let test = LoggerClient { message in | ||
lastLoggedMessage = message | ||
} | ||
} | ||
|
||
@MainActor |
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.
@MainActor |
looks like this isn't necessary
} | ||
|
||
@MainActor |
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.
@MainActor |
same here
@testable import KlaviyoSwift | ||
import Combine | ||
@preconcurrency import Combine // Will figure out a better way for this... |
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.
did you intend to leave this comment in?
Task { | ||
let script = await WKUserScript(source: closeHandlerScript, injectionTime: .atDocumentEnd, forMainFrameOnly: true) | ||
scripts["closeHandler"] = script | ||
} |
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.
hmm, by dispatching this to a task, the KlaviyoWebView
will read the loadScripts
property before the closeHandler
script has been loaded. I can think some more tomorrow about how to resolve this; it might involve some refactoring of the View/ViewModel
Description
This will update us to the latest TCA which should get us to full ios 18 and swift 6 support with strict concurrency mode.
Check List
Manual Test Plan
Supporting Materials