Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ on:

jobs:
mac-os:
runs-on: macos-15
runs-on: macos-26
strategy:
matrix:
xcode: ["16.0"]
xcode: ["26.4.1"]
steps:
- uses: maxim-lobanov/setup-xcode@v1
with:
Expand Down
12 changes: 12 additions & 0 deletions .swift-format
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"indentation": {
"spaces": 4
},
"lineLength": 120,
"multilineTrailingCommaBehavior": "alwaysUsed",
"multiElementCollectionTrailingCommas": false,
"rules": {
"NoAccessLevelOnExtensionDeclaration": false
},
"version": 1
}
15 changes: 15 additions & 0 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 36 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@ let package = Package(
name: "DataCacheKit",
targets: ["DataCacheKit"]),
],
dependencies: [],
dependencies: [
// .package(url: "https://github.com/swiftty/swift-project-starter.git", from: "0.2.0"),
// AUTO GENERATED ↓: swift-project-starter: deps
.package(url: "https://github.com/swiftty/swift-format-plugin", from: "1.0.0")
// AUTO GENERATED ↑: swift-project-starter: deps
],
targets: [
.target(
name: "DataCacheKit",
Expand All @@ -34,3 +39,33 @@ let package = Package(
dependencies: ["LRUCache"]),
]
)

// AUTO GENERATED ↓: swift-project-starter: settings
for target in package.targets {
if [.executable, .test, .regular].contains(target.type) {
do {
var swiftSettings = target.swiftSettings ?? []
defer {
target.swiftSettings = swiftSettings
}
swiftSettings += [
.enableUpcomingFeature("InternalImportsByDefault"),
.enableUpcomingFeature("NonisolatedNonsendingByDefault"),
.enableUpcomingFeature("MemberImportVisibility"),
.enableUpcomingFeature("InferIsolatedConformances"),
.enableUpcomingFeature("ImmutableWeakCaptures"),
.enableUpcomingFeature("ExistentialAny")
]
}
do {
var plugins = target.plugins ?? []
defer {
target.plugins = plugins
}
plugins += [
.plugin(name: "Lint", package: "swift-format-plugin")
]
}
}
}
Comment on lines +44 to +70
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The loop to apply global settings can be simplified by removing the do blocks and defer statements, which makes the configuration more concise and readable.

for target in package.targets {
    if [.executable, .test, .regular].contains(target.type) {
        target.swiftSettings = (target.swiftSettings ?? []) + [
            .enableUpcomingFeature("InternalImportsByDefault"),
            .enableUpcomingFeature("NonisolatedNonsendingByDefault"),
            .enableUpcomingFeature("MemberImportVisibility"),
            .enableUpcomingFeature("InferIsolatedConformances"),
            .enableUpcomingFeature("ImmutableWeakCaptures"),
            .enableUpcomingFeature("ExistentialAny")
        ]
        target.plugins = (target.plugins ?? []) + [
            .plugin(name: "Lint", package: "swift-format-plugin")
        ]
    }
}

// AUTO GENERATED ↑: swift-project-starter: settings
4 changes: 2 additions & 2 deletions Sources/DataCacheKit/Cache.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Foundation
import OSLog
public import OSLog

public actor Cache<Key: Hashable & Sendable, Value: Codable & Sendable>: Caching {
public struct Options: Sendable {
Expand All @@ -15,7 +15,7 @@ public actor Cache<Key: Hashable & Sendable, Value: Codable & Sendable>: Caching
public nonisolated let options: Options
public nonisolated let logger: Logger

public subscript (key: Key) -> Value? {
public subscript(key: Key) -> Value? {
get async throws {
try await value(for: key)
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/DataCacheKit/Caching.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ public protocol Caching<Key, Value>: Actor {
associatedtype Key: Hashable & Sendable
associatedtype Value

subscript (key: Key) -> Value? { get async throws }
subscript(key: Key) -> Value? { get async throws }

func value(for key: Key) async throws -> Value?

Expand Down
6 changes: 3 additions & 3 deletions Sources/DataCacheKit/DiskCache+Options.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Foundation
public import Foundation
import CommonCrypto

extension DiskCache {
Expand All @@ -17,15 +17,15 @@ extension DiskCache {
self.init(
sizeLimit: 150 * 1024 * 1024,
filename: defaultFilename(for:),
path: path
path: path,
)
}

public init(
sizeLimit: Int,
filename: @escaping @Sendable (Key) -> String?,
path: Path,
expirationTimeout: TimeInterval? = nil
expirationTimeout: TimeInterval? = nil,
) {
self.sizeLimit = sizeLimit
self.filename = filename
Expand Down
55 changes: 30 additions & 25 deletions Sources/DataCacheKit/DiskCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
// This implementation is based on kean/Nuke's DataCache
// https://github.com/kean/Nuke/blob/master/Sources/Core/Caching/DataCache.swift

import Foundation
import OSLog
public import Foundation
public import OSLog

// MARK: - DiskCache
public actor DiskCache<Key: Hashable & Sendable>: Caching, @unchecked Sendable {
Expand All @@ -14,17 +14,17 @@ public actor DiskCache<Key: Hashable & Sendable>: Caching, @unchecked Sendable {
public nonisolated let options: Options
public nonisolated let logger: Logger

public subscript (key: Key) -> Data? {
public subscript(key: Key) -> Data? {
get async throws {
try await value(for: key)
}
// set {
// if let newValue {
// storeData(newValue, for: key)
// } else {
// removeData(for: key)
// }
// }
// set {
// if let newValue {
// storeData(newValue, for: key)
// } else {
// removeData(for: key)
// }
// }
}

private let clock: any Clock<Duration>
Expand All @@ -42,11 +42,11 @@ public actor DiskCache<Key: Hashable & Sendable>: Caching, @unchecked Sendable {

private(set) lazy var staging = Staging<Key>()

private var runningTasks: [Key: Task<Void, Error>] = [:]
private var runningTasks: [Key: Task<Void, any Error>] = [:]

private(set) var flushingTask: Task<Void, Error>?
private(set) var flushingTask: Task<Void, any Error>?

private(set) var sweepingTask: Task<Void, Error>?
private(set) var sweepingTask: Task<Void, any Error>?

private(set) var isFlushNeeded = false

Expand Down Expand Up @@ -77,7 +77,7 @@ public actor DiskCache<Key: Hashable & Sendable>: Caching, @unchecked Sendable {
func value(for key: Key, with now: Date) async throws -> Data? {
await Task.yield()

let task = Task<Data?, Error> {
let task = Task<Data?, any Error> {
_ = await queueingTask?.result

for stage in staging.stages.reversed() {
Expand All @@ -96,7 +96,7 @@ public actor DiskCache<Key: Hashable & Sendable>: Caching, @unchecked Sendable {

await waitForTask(for: key)

let task = Task<Data?, Error>.detached {
let task = Task<Data?, any Error>.detached {
do {
let data = try Data(contentsOf: url)

Expand All @@ -117,7 +117,6 @@ public actor DiskCache<Key: Hashable & Sendable>: Caching, @unchecked Sendable {
return try await task.value
}


@discardableResult
public nonisolated func store(_ value: Value, for key: Key) -> Task<Void, Never> {
return Task {
Expand Down Expand Up @@ -178,7 +177,8 @@ public actor DiskCache<Key: Hashable & Sendable>: Caching, @unchecked Sendable {
let dir: URL?
switch options.path {
case .default(let name):
dir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first?.appendingPathComponent(name)
dir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first?.appendingPathComponent(
name)

case .custom(let url):
dir = url
Expand Down Expand Up @@ -228,7 +228,7 @@ extension DiskCache {
}
}

private func flushIfNeeded(_ oldTask: Task<Void, Error>?) async throws {
private func flushIfNeeded(_ oldTask: Task<Void, any Error>?) async throws {
guard !isFlushScheduled else { return }
isFlushScheduled = true
defer { isFlushScheduled = false }
Expand Down Expand Up @@ -292,13 +292,14 @@ extension DiskCache {
}
}

private func peformChange(_ change: Staging<Key>.Change, with url: URL) -> Task<Void, Error> {
private func peformChange(_ change: Staging<Key>.Change, with url: URL) -> Task<Void, any Error> {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Typo in method name: peformChange should be performChange. Note that you should also update the call site in the _flush method.

Suggested change
private func peformChange(_ change: Staging<Key>.Change, with url: URL) -> Task<Void, any Error> {
private func performChange(_ change: Staging<Key>.Change, with url: URL) -> Task<Void, any Error> {

let task = Task {
do {
switch change.operation {
case .add(let data):
if case let dir = url.deletingLastPathComponent(),
!FileManager.default.fileExists(atPath: dir.path) {
!FileManager.default.fileExists(atPath: dir.path)
{
Comment on lines 300 to +302
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Using if case let for a non-optional assignment is non-idiomatic in Swift. It is clearer and more readable to perform the assignment before the if statement.

                    let dir = url.deletingLastPathComponent()
                    if !FileManager.default.fileExists(atPath: dir.path) {

try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
}

Expand All @@ -324,7 +325,7 @@ extension DiskCache {
return task
}

private func performChangeRemoveAll(for changes: some Collection<Staging<Key>.Change>) -> Task<Void, Error> {
private func performChangeRemoveAll(for changes: some Collection<Staging<Key>.Change>) -> Task<Void, any Error> {
let task = Task {
do {
let dir = try path
Expand Down Expand Up @@ -376,10 +377,13 @@ extension DiskCache {
do {
try FileManager.default.removeItem(at: item.url)
size -= item.meta.totalFileAllocatedSize ?? 0
logger.debug("\(self.logKey)sweeped item: \(item.url.lastPathComponent), size: \(item.meta.totalFileAllocatedSize ?? 0)")
logger.debug(
"\(self.logKey)sweeped item: \(item.url.lastPathComponent), size: \(item.meta.totalFileAllocatedSize ?? 0)"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Typo in log message: sweeped should be swept.

Suggested change
"\(self.logKey)sweeped item: \(item.url.lastPathComponent), size: \(item.meta.totalFileAllocatedSize ?? 0)"
"\(self.logKey)swept item: \(item.url.lastPathComponent), size: \(item.meta.totalFileAllocatedSize ?? 0)"

)
return true
} catch {
logger.error("\(self.logKey)sweep item: \(item.url.lastPathComponent), error: \(String(describing: error))")
logger.error(
"\(self.logKey)sweep item: \(item.url.lastPathComponent), error: \(String(describing: error))")
return false
}
}
Expand Down Expand Up @@ -500,7 +504,7 @@ struct Staging<Key: Hashable & Sendable> {
mutating func remove(for key: Key) {
let change = Change(key: key, id: changeID.nextID(), operation: .remove)
if checkConflicts(on: key) {
stages.append(Stage(id: stageID.nextID(),changes: [key: change]))
stages.append(Stage(id: stageID.nextID(), changes: [key: change]))
} else {
stages[stages.count - 1].changes[key] = change
}
Expand All @@ -520,7 +524,8 @@ struct Staging<Key: Hashable & Sendable> {
stages.append(stage)
}

mutating func flushed(id: Int, changes: [Change], with logger: Logger, logKey: @autoclosure @escaping () -> String) {
mutating func flushed(id: Int, changes: [Change], with logger: Logger, logKey: @autoclosure @escaping () -> String)
{
guard case (let i, var stage)? = stages.enumerated().first(where: { $1.id == id }) else {
assert(changes.isEmpty)
return
Expand Down
4 changes: 2 additions & 2 deletions Sources/DataCacheKit/MemoryCache+Options.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Foundation
public import Foundation

extension MemoryCache {
public struct Options: Sendable {
Expand All @@ -17,7 +17,7 @@ extension MemoryCache {
extension MemoryCache.Options where Value == Data {
public init(
countLimit: Int,
sizeLimit: Int
sizeLimit: Int,
) {
self.countLimit = countLimit
self.sizeLimit = sizeLimit
Expand Down
4 changes: 2 additions & 2 deletions Sources/DataCacheKit/MemoryCache.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Foundation
import OSLog
public import OSLog
import LRUCache

public actor MemoryCache<Key: Hashable & Sendable, Value: Sendable>: Caching {
Expand All @@ -9,7 +9,7 @@ public actor MemoryCache<Key: Hashable & Sendable, Value: Sendable>: Caching {
private let lruCache = LRUCache<Key, Value>()
private var queueingTask: Task<Void, Never>?

public subscript (key: Key) -> Value? {
public subscript(key: Key) -> Value? {
get async {
await value(for: key)
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/LRUCache/LRUCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public struct LRUCache<Key: Hashable & Sendable, Value: Sendable>: ~Copyable, Se
}

extension LRUCache {
public subscript (_ key: Key, cost cost: Int = 0) -> Value? {
public subscript(_ key: Key, cost cost: Int = 0) -> Value? {
get {
value(forKey: key)
}
Expand Down
5 changes: 4 additions & 1 deletion Tests/DataCacheKitTests/DiskCacheTests.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import Testing
import Foundation
@testable import DataCacheKit
import os

func yield(until condition: @autoclosure () async -> Bool, message: @autoclosure () -> String? = nil, limit: Int = 10000) async throws {
func yield(
until condition: @autoclosure () async -> Bool, message: @autoclosure () -> String? = nil, limit: Int = 10000,
) async throws {
var limit = limit
while limit > 0 {
limit -= 1
Expand Down
Loading