Skip to content

Update to use newer Cache version#144

Closed
0xLeif wants to merge 3 commits intomainfrom
leif/updates
Closed

Update to use newer Cache version#144
0xLeif wants to merge 3 commits intomainfrom
leif/updates

Conversation

@0xLeif
Copy link
Owner

@0xLeif 0xLeif commented Dec 12, 2025

This pull request introduces significant improvements to concurrency safety and platform support for the AppState library. The most important changes are the adoption of a new Mutex-based locking mechanism for thread safety, updates to platform and Swift version requirements, and the addition of comprehensive stress tests to verify concurrency robustness. Documentation has also been updated to highlight new examples.

Concurrency and Thread Safety Improvements:

  • Replaced all uses of NSLock/NSRecursiveLock in the Keychain and Application classes with a new Mutex-based locking mechanism provided by the updated Cache dependency, improving thread safety and removing manual locking code. (Sources/AppState/Dependencies/Keychain.swift, Sources/AppState/Application/Application.swift, Sources/AppState/Application/Application+internal.swift) [1] [2] [3] [4] [5] [6] [7] [8] [9] [10]

  • Updated the Cache dependency to use the leif/mutex branch, which provides the new Mutex implementation. (Package.swift)

Platform and Language Support:

  • Increased minimum supported platform versions (iOS 18, macOS 15, watchOS 11, tvOS 18, visionOS 2) and enabled Swift 6 language mode with the StrictConcurrency experimental feature for both library and test targets. (Package.swift) [1] [2]

Testing Enhancements:

  • Added a new ConcurrencyStressTests test suite to verify thread safety and concurrency robustness of state and dependency management, including high-contention scenarios and rapid state resets. These tests also cover concurrent access to the Keychain. (Tests/AppStateTests/ConcurrencyStressTests.swift)

Documentation and Examples:

  • Expanded the README.md with a new "Examples" section, listing 31 categorized example projects and highlighting featured apps, to help users explore AppState's capabilities. (README.md)

These changes collectively modernize the codebase, ensure robust concurrency handling, and provide better documentation and testing for users and contributors.

@0xLeif 0xLeif requested a review from Copilot December 12, 2025 03:35
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @0xLeif, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request modernizes the AppState library by upgrading its core concurrency mechanisms, ensuring robust thread safety across its components. It also updates platform and language requirements to leverage the latest Swift features and significantly enhances the test suite with comprehensive concurrency stress tests. These changes collectively aim to provide a more stable, performant, and future-proof foundation for the library, alongside improved documentation for better user experience.

Highlights

  • Concurrency and Thread Safety: Replaced NSLock/NSRecursiveLock with a new Mutex-based locking mechanism from an updated Cache dependency in Keychain and Application classes, significantly improving thread safety and simplifying locking code.
  • Platform and Language Support: Increased minimum supported platform versions (iOS 18, macOS 15, watchOS 11, tvOS 18, visionOS 2) and enabled Swift 6 language mode with the StrictConcurrency experimental feature for both library and test targets.
  • Testing Enhancements: Introduced a new ConcurrencyStressTests suite to rigorously verify thread safety and concurrency robustness of state and dependency management, including high-contention scenarios and concurrent Keychain access.
  • Documentation and Examples: Expanded the README.md with a new 'Examples' section, showcasing 31 categorized example projects and highlighting featured applications to aid user understanding and exploration of AppState's capabilities.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a 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 makes significant improvements to thread safety by replacing manual locking with a Mutex from an updated Cache dependency. The changes are well-implemented, and the removal of NSLock and @MainActor annotations where appropriate is correct. The addition of concurrency stress tests is a great initiative to validate these changes.

My review focuses on two main points. First, the Package.swift file now depends on a Git branch, which is a risk for build stability and should be changed to a version tag. Second, many of the new concurrency stress tests are not actually testing concurrent execution because the tasks are serialized on the main actor. This undermines their goal of verifying thread safety under parallel load. I've provided detailed feedback on these points.

],
dependencies: [
.package(url: "https://github.com/0xLeif/Cache", from: "2.0.0"),
.package(url: "https://github.com/0xLeif/Cache", branch: "leif/mutex"),
Copy link
Contributor

Choose a reason for hiding this comment

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

high

Using a git branch for a dependency is generally discouraged for shared or production code as it can lead to build instability. Branches can be force-pushed, rebased, or deleted, which would break builds for anyone using this package. For better stability and reproducible builds, it's recommended to depend on a specific version tag or a commit hash. Please consider updating this to a stable release version once the leif/mutex branch is merged and a new version of Cache is tagged.

Comment on lines +51 to +54
group.addTask { @MainActor in
var state = Application.state(\.stressString)
state.value = "value-\(i)"
}
Copy link
Contributor

Choose a reason for hiding this comment

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

high

The tasks within this test group are marked with @MainActor. This means all write operations will be serialized on the main thread, and the test will not actually perform concurrent writes. This defeats the purpose of a concurrency stress test, which is to verify thread safety under parallel execution. This applies to several other tests in this file as well (e.g., testHighContentionStateUpdates, testConcurrentArrayModifications).

While Application.State.value is @MainActor, which forces these writes to the main thread, the test name ConcurrencyStressTests is misleading. The testKeychainConcurrentAccess function is a good example of a true concurrency test because its tasks are not actor-isolated and execute in parallel.

Please either rename these tests to reflect that they test high-volume serial operations on the main actor, or refactor them to test true concurrency if there's a way to do so while respecting the library's API design.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR modernizes the AppState library for Swift 6 by replacing manual locking mechanisms with Mutex-based concurrency primitives, significantly raising minimum platform requirements, and adding comprehensive stress tests to validate thread safety. The changes enable Swift 6 strict concurrency checking and prepare the codebase for modern Swift development.

  • Migrated from NSLock/NSRecursiveLock to Mutex from the Synchronization module for thread-safe state access
  • Updated minimum platform versions to iOS 18, macOS 15, watchOS 11, tvOS 18, and visionOS 2
  • Added comprehensive concurrency stress tests covering state management, dependencies, and Keychain operations

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
Package.swift Updated platform requirements to latest versions, switched Cache dependency to leif/mutex branch, and enabled Swift 6 language mode with StrictConcurrency
Sources/AppState/Dependencies/Keychain.swift Replaced NSLock with Mutex<Set>, removed @mainactor annotations, and simplified thread-safe operations using withLock
Sources/AppState/Application/Application.swift Removed NSRecursiveLock property that was previously used for manual locking
Sources/AppState/Application/Application+internal.swift Simplified value() method by removing manual lock/unlock calls, now relying on Cache's internal Mutex
Tests/AppStateTests/ConcurrencyStressTests.swift Added new comprehensive stress test suite with 8 tests covering concurrent state operations, dependency access, and Keychain operations
README.md Added Examples section documenting 31 example projects organized by complexity level with featured app highlights
Comments suppressed due to low confidence (1)

Package.swift:21

  • Using a branch dependency instead of a versioned release can lead to build instability. The "leif/mutex" branch could change or be deleted without warning, breaking builds for downstream users. Consider requesting a tagged release of the Cache library with the Mutex functionality and updating this to use a version requirement like from: "x.x.x" instead.
        .package(url: "https://github.com/0xLeif/Cache", branch: "leif/mutex"),

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

README.md Outdated
Comment on lines 75 to 92
## Examples

Explore our comprehensive [Examples](Examples/) folder with 31 example projects:

| Category | Examples | Description |
|----------|----------|-------------|
| **Focused** | 2 | Production-quality apps (SyncNotes, MultiPlatformTracker) |
| **Moderate** | 4 | Feature-focused apps (TodoCloud, SettingsKit, DataDashboard, SecureVault) |
| **Lightweight** | 25 | Single-concept examples covering all AppState features |

### Featured Examples

- **[SyncNotes](Examples/Focused/SyncNotes/)** - Note-taking with iCloud sync across devices
- **[MultiPlatformTracker](Examples/Focused/MultiPlatformTracker/)** - Habit tracker for iOS/macOS/watchOS
- **[TodoCloud](Examples/Moderate/TodoCloud/)** - Progressive persistence (memory → local → cloud)
- **[SecureVault](Examples/Moderate/SecureVault/)** - Password manager with Keychain storage

See [Examples/README.md](Examples/README.md) for the full list with descriptions.
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

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

The README references an "Examples/" directory and specific example projects that don't exist in the repository. All the links to Examples/Focused/, Examples/Moderate/, and Examples/README.md will be broken. Either the Examples directory needs to be added in this PR, or these references should be removed or marked as "coming soon".

Copilot uses AI. Check for mistakes.
Comment on lines 211 to 220
group.addTask {
keychain.set(value: "value-\(i)", forKey: "stress-key-\(i % 10)")
}
}

// Concurrent reads
for i in 0..<iterations {
group.addTask {
_ = keychain.get("stress-key-\(i % 10)")
}
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

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

The tasks within this withTaskGroup are marked @mainactor, but the test creates non-isolated concurrent tasks that shouldn't necessarily run on the MainActor. Since the Keychain methods are now properly thread-safe with Mutex and don't require @mainactor, the @mainactor annotation on the task closures should be removed to allow proper concurrent execution. This applies to both the write tasks (lines 211-213) and read tasks (lines 218-220).

Suggested change
group.addTask {
keychain.set(value: "value-\(i)", forKey: "stress-key-\(i % 10)")
}
}
// Concurrent reads
for i in 0..<iterations {
group.addTask {
_ = keychain.get("stress-key-\(i % 10)")
}
group.addTask(@Sendable {
keychain.set(value: "value-\(i)", forKey: "stress-key-\(i % 10)")
})
}
// Concurrent reads
for i in 0..<iterations {
group.addTask(@Sendable {
_ = keychain.get("stress-key-\(i % 10)")
})

Copilot uses AI. Check for mistakes.
Comment on lines 242 to 256
await withTaskGroup(of: Void.self) { group in
// Writers
for i in 0..<iterations {
group.addTask {
keychain.set(value: "v\(i)", forKey: "concurrent-\(i)")
}
}

// Readers calling values()
for _ in 0..<iterations {
group.addTask {
_ = keychain.values()
}
}
}
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

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

Similar to the test above, the tasks within this withTaskGroup don't need to be confined to the MainActor since Keychain operations are now thread-safe with Mutex. The @mainactor annotations on the task closures (lines 245 and 252) should be removed to enable true concurrent execution and better stress testing.

Copilot uses AI. Check for mistakes.
],
dependencies: [
.package(url: "https://github.com/0xLeif/Cache", from: "2.0.0"),
.package(url: "https://github.com/0xLeif/Cache", branch: "leif/mutex"),
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

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

The Cache dependency is pinned to a mutable Git branch (branch: "leif/mutex"), which creates a supply chain risk because future builds can silently pull different, attacker-controlled code without any change to this repository. An attacker who compromises the upstream repo or branch could inject malicious logic into your library at build time. To mitigate this, pin the dependency to an immutable reference such as a released semantic version or a specific commit SHA and update it explicitly when needed.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.

Comments suppressed due to low confidence (1)

Package.swift:21

  • The Cache dependency is pinned to a specific branch ("leif/mutex") instead of a semantic version. Branch dependencies are unstable and can change without notice, potentially breaking builds or introducing unexpected behavior. This is generally considered bad practice for production code as it makes builds non-reproducible. Once the mutex functionality is merged and released in Cache, this should be updated to use a semantic version (e.g., from: "X.Y.Z").
        .package(url: "https://github.com/0xLeif/Cache", branch: "leif/mutex"),

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Task { @MainActor in
keys.insert(key)
}
_ = keys.withLock { $0.insert(key) }
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

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

The key is added to the internal keys set regardless of whether the keychain operation succeeds. If SecItemAdd fails for reasons other than the item already existing (e.g., keychain access denied, disk full), the key will still be tracked in the set even though the value isn't actually stored in the keychain. This could lead to inconsistencies between the tracked keys and actual keychain contents. Consider only adding the key to the set if the keychain operation succeeds (status == errSecSuccess).

Copilot uses AI. Check for mistakes.
Comment on lines 123 to +124
SecItemDelete(query as CFDictionary)
lock.unlock()
_ = keys.withLock { $0.remove(key) }
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

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

The key is removed from the internal keys set regardless of whether SecItemDelete succeeds. If the deletion fails (e.g., item doesn't exist in keychain, access denied), the key will still be removed from the tracked set, causing the internal state to be inconsistent with the actual keychain contents. Consider only removing the key from the set if SecItemDelete succeeds (status == errSecSuccess) or if the item doesn't exist (status == errSecItemNotFound).

Copilot uses AI. Check for mistakes.

XCTAssertEqual(completedWrites, iterations)
let finalState = Application.state(\.stressCounter)
XCTAssertGreaterThan(finalState.value, 0)
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

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

The test performs 100 read-modify-write operations sequentially on @mainactor. Since these operations are serialized by @mainactor, the final value should deterministically equal 100 (iterations). The current assertion only checks if the value is greater than 0, which is a weak validation that would pass even if only 1 operation succeeded. Consider changing this to XCTAssertEqual(finalState.value, iterations) to properly verify that all updates were applied correctly.

Copilot uses AI. Check for mistakes.
}

let finalState = Application.state(\.stressArray)
XCTAssertGreaterThan(finalState.value.count, 0)
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

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

The test spawns 200 tasks that each read the array, append an element, and write it back. Since all tasks run sequentially on @mainactor, the final array should contain exactly 200 elements (one from each task). However, the assertion only checks that the count is greater than 0, which would pass even if only a single append succeeded. Consider changing line 118 to XCTAssertEqual(finalState.value.count, iterations) to properly verify that all modifications were applied.

Copilot uses AI. Check for mistakes.
@0xLeif 0xLeif closed this Dec 13, 2025
@0xLeif 0xLeif deleted the leif/updates branch December 13, 2025 02:21
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.

1 participant