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

Update TCA to latest version #228

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
Open

Update TCA to latest version #228

wants to merge 11 commits into from

Conversation

ndurell
Copy link
Contributor

@ndurell ndurell commented Oct 24, 2024

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

  • Are you changing anything with the public API?
  • Have you tested this change on real device?
  • Are your changes backwards compatible with previous SDK Versions?
  • Have you added unit test coverage for your changes?
  • Have you verified that your changes are compatible with all the operating system version this SDK currently supports?

Manual Test Plan

Supporting Materials

@klaviyo klaviyo deleted a comment from nightfall-for-github bot Nov 5, 2024
@ndurell ndurell marked this pull request as ready for review November 6, 2024 15:14
@ndurell ndurell requested a review from a team as a code owner November 6, 2024 15:14
@ndurell ndurell changed the title [DNM] - Update TCA to latest version Update TCA to latest version Nov 6, 2024
@ndurell ndurell requested a review from ajaysubra November 6, 2024 15:15
@@ -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
Copy link
Contributor Author

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.

.swiftformat Outdated Show resolved Hide resolved
.swiftlint.yml Outdated Show resolved Hide resolved
@@ -1,11 +1,12 @@
// swift-tools-version: 5.6
// swift-tools-version:5.9
Copy link
Contributor Author

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)],
Copy link
Contributor Author

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.

Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor Author

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(
Copy link
Contributor Author

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()
Copy link
Contributor Author

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)
Copy link
Contributor Author

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.")
Copy link
Contributor Author

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):
Copy link
Contributor Author

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() {
Copy link
Contributor Author

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)
Copy link
Contributor Author

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)
Copy link
Contributor Author

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
Copy link
Contributor Author

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
Copy link
Contributor Author

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
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Needs to be Sendable.

@@ -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
Copy link
Contributor Author

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.

Makefile Show resolved Hide resolved
@ndurell
Copy link
Contributor Author

ndurell commented Nov 14, 2024

Can we add comments to all TCA files to indicate that they were either copied verbatim from TCA or modified from TCA, and add the TCA version number that the file was copied from? I have already run into situations where I've tried to diff our local TCA files from the ones in the TCA repo, and having the version number right in the comments have been very helpful to know exactly where the code came from.

For example, at the top of a file that was copied verbatim from TCA, we might add :

/// Copied verbatim from TCA v1.15.2 on 11/14/2024
/// https://github.com/pointfreeco/swift-composable-architecture/tree/1.15.2

And if the file has been modified from the original TCA file, we might add:

/// Adapted from TCA v1.15.2 on 11/14/2024
/// https://github.com/pointfreeco/swift-composable-architecture/tree/1.15.2
/// Changes from TCA:
/// <add brief summary of what's different between our local version and the official TCA version>

💯 great idea!

Sources/KlaviyoCore/Models/PushBackground.swift Outdated Show resolved Hide resolved
Sources/KlaviyoCore/KlaviyoEnvironment.swift Outdated Show resolved Hide resolved
Copy link
Contributor

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?

Copy link
Contributor Author

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?

@ndurell ndurell removed the request for review from jasper-hsu-kyvo November 15, 2024 20:34
@ndurell ndurell force-pushed the noah/update_tca branch 4 times, most recently from aee9f46 to 1c3b070 Compare November 27, 2024 18:23
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 }
Copy link
Contributor

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?

Copy link
Contributor Author

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 {
Copy link
Contributor

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)

Copy link
Contributor Author

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 {
Copy link
Contributor

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.

Copy link
Contributor Author

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 {
Copy link
Contributor

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?

Copy link
Contributor Author

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 {
Copy link
Contributor

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

Copy link
Contributor Author

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.

@dan-peluso
Copy link
Contributor

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

@ndurell
Copy link
Contributor Author

ndurell commented Dec 10, 2024

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!

Copy link
Contributor

@ab1470 ab1470 left a 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

Copy link
Contributor

@ab1470 ab1470 left a 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

Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change

Nit: remove a space to make sure that inline documentation works properly

}
}
}
}, appContextInfo: {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
}, appContextInfo: {
},
appContextInfo: {

nit, aligning parameter names

}

public enum KlaviyoDecodingError: Error {
case invalidType
case invalidJson
}

public struct DataDecoder {
public struct DataDecoder: @unchecked Sendable {
Copy link
Contributor

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?

Copy link
Contributor

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]? {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
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())
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
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() },
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
}, networkSession: { createNetworkSession() },
},
networkSession: { createNetworkSession() },

nit, aligning all parameters

extension LoggerClient {
static var lastLoggedMessage: String?
static let test = LoggerClient { message in
lastLoggedMessage = message
}
}

@MainActor
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
@MainActor

looks like this isn't necessary

}

@MainActor
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
@MainActor

same here

@testable import KlaviyoSwift
import Combine
@preconcurrency import Combine // Will figure out a better way for this...
Copy link
Contributor

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?

Comment on lines +36 to +39
Task {
let script = await WKUserScript(source: closeHandlerScript, injectionTime: .atDocumentEnd, forMainFrameOnly: true)
scripts["closeHandler"] = script
}
Copy link
Contributor

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

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.

3 participants