diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7a25ab2..5ed89a2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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: diff --git a/.swift-format b/.swift-format new file mode 100644 index 0000000..ec200d4 --- /dev/null +++ b/.swift-format @@ -0,0 +1,12 @@ +{ + "indentation": { + "spaces": 4 + }, + "lineLength": 120, + "multilineTrailingCommaBehavior": "alwaysUsed", + "multiElementCollectionTrailingCommas": false, + "rules": { + "NoAccessLevelOnExtensionDeclaration": false + }, + "version": 1 +} diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..f133be8 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "3828e0f1527b7f4bb8274c0dd93194513f6d443e1d944aa438df003adab8d76d", + "pins" : [ + { + "identity" : "swift-format-plugin", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftty/swift-format-plugin", + "state" : { + "revision" : "962d1a34765c0eff20a4a13d6e6c1128c20c61c5", + "version" : "1.0.0" + } + } + ], + "version" : 3 +} diff --git a/Package.swift b/Package.swift index 47b6113..8acbdeb 100644 --- a/Package.swift +++ b/Package.swift @@ -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", @@ -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") + ] + } + } +} +// AUTO GENERATED ↑: swift-project-starter: settings diff --git a/Sources/DataCacheKit/Cache.swift b/Sources/DataCacheKit/Cache.swift index 829bef2..cef8ba3 100644 --- a/Sources/DataCacheKit/Cache.swift +++ b/Sources/DataCacheKit/Cache.swift @@ -1,5 +1,5 @@ import Foundation -import OSLog +public import OSLog public actor Cache: Caching { public struct Options: Sendable { @@ -15,7 +15,7 @@ public actor Cache: 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) } diff --git a/Sources/DataCacheKit/Caching.swift b/Sources/DataCacheKit/Caching.swift index 6ba64b8..6e66715 100644 --- a/Sources/DataCacheKit/Caching.swift +++ b/Sources/DataCacheKit/Caching.swift @@ -4,7 +4,7 @@ public protocol Caching: 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? diff --git a/Sources/DataCacheKit/DiskCache+Options.swift b/Sources/DataCacheKit/DiskCache+Options.swift index 317c737..38511e5 100644 --- a/Sources/DataCacheKit/DiskCache+Options.swift +++ b/Sources/DataCacheKit/DiskCache+Options.swift @@ -1,4 +1,4 @@ -import Foundation +public import Foundation import CommonCrypto extension DiskCache { @@ -17,7 +17,7 @@ extension DiskCache { self.init( sizeLimit: 150 * 1024 * 1024, filename: defaultFilename(for:), - path: path + path: path, ) } @@ -25,7 +25,7 @@ extension DiskCache { sizeLimit: Int, filename: @escaping @Sendable (Key) -> String?, path: Path, - expirationTimeout: TimeInterval? = nil + expirationTimeout: TimeInterval? = nil, ) { self.sizeLimit = sizeLimit self.filename = filename diff --git a/Sources/DataCacheKit/DiskCache.swift b/Sources/DataCacheKit/DiskCache.swift index 0696581..f41fcd3 100644 --- a/Sources/DataCacheKit/DiskCache.swift +++ b/Sources/DataCacheKit/DiskCache.swift @@ -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: Caching, @unchecked Sendable { @@ -14,17 +14,17 @@ public actor DiskCache: 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 @@ -42,11 +42,11 @@ public actor DiskCache: Caching, @unchecked Sendable { private(set) lazy var staging = Staging() - private var runningTasks: [Key: Task] = [:] + private var runningTasks: [Key: Task] = [:] - private(set) var flushingTask: Task? + private(set) var flushingTask: Task? - private(set) var sweepingTask: Task? + private(set) var sweepingTask: Task? private(set) var isFlushNeeded = false @@ -77,7 +77,7 @@ public actor DiskCache: Caching, @unchecked Sendable { func value(for key: Key, with now: Date) async throws -> Data? { await Task.yield() - let task = Task { + let task = Task { _ = await queueingTask?.result for stage in staging.stages.reversed() { @@ -96,7 +96,7 @@ public actor DiskCache: Caching, @unchecked Sendable { await waitForTask(for: key) - let task = Task.detached { + let task = Task.detached { do { let data = try Data(contentsOf: url) @@ -117,7 +117,6 @@ public actor DiskCache: Caching, @unchecked Sendable { return try await task.value } - @discardableResult public nonisolated func store(_ value: Value, for key: Key) -> Task { return Task { @@ -178,7 +177,8 @@ public actor DiskCache: 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 @@ -228,7 +228,7 @@ extension DiskCache { } } - private func flushIfNeeded(_ oldTask: Task?) async throws { + private func flushIfNeeded(_ oldTask: Task?) async throws { guard !isFlushScheduled else { return } isFlushScheduled = true defer { isFlushScheduled = false } @@ -292,13 +292,14 @@ extension DiskCache { } } - private func peformChange(_ change: Staging.Change, with url: URL) -> Task { + private func peformChange(_ change: Staging.Change, with url: URL) -> Task { 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) + { try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) } @@ -324,7 +325,7 @@ extension DiskCache { return task } - private func performChangeRemoveAll(for changes: some Collection.Change>) -> Task { + private func performChangeRemoveAll(for changes: some Collection.Change>) -> Task { let task = Task { do { let dir = try path @@ -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)" + ) 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 } } @@ -500,7 +504,7 @@ struct Staging { 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 } @@ -520,7 +524,8 @@ struct Staging { 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 diff --git a/Sources/DataCacheKit/MemoryCache+Options.swift b/Sources/DataCacheKit/MemoryCache+Options.swift index c015a85..a468de3 100644 --- a/Sources/DataCacheKit/MemoryCache+Options.swift +++ b/Sources/DataCacheKit/MemoryCache+Options.swift @@ -1,4 +1,4 @@ -import Foundation +public import Foundation extension MemoryCache { public struct Options: Sendable { @@ -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 diff --git a/Sources/DataCacheKit/MemoryCache.swift b/Sources/DataCacheKit/MemoryCache.swift index 36b538a..8b91b0f 100644 --- a/Sources/DataCacheKit/MemoryCache.swift +++ b/Sources/DataCacheKit/MemoryCache.swift @@ -1,5 +1,5 @@ import Foundation -import OSLog +public import OSLog import LRUCache public actor MemoryCache: Caching { @@ -9,7 +9,7 @@ public actor MemoryCache: Caching { private let lruCache = LRUCache() private var queueingTask: Task? - public subscript (key: Key) -> Value? { + public subscript(key: Key) -> Value? { get async { await value(for: key) } diff --git a/Sources/LRUCache/LRUCache.swift b/Sources/LRUCache/LRUCache.swift index 87af5ee..1e242ed 100644 --- a/Sources/LRUCache/LRUCache.swift +++ b/Sources/LRUCache/LRUCache.swift @@ -52,7 +52,7 @@ public struct LRUCache: ~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) } diff --git a/Tests/DataCacheKitTests/DiskCacheTests.swift b/Tests/DataCacheKitTests/DiskCacheTests.swift index 43f9b6b..ac89775 100644 --- a/Tests/DataCacheKitTests/DiskCacheTests.swift +++ b/Tests/DataCacheKitTests/DiskCacheTests.swift @@ -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 diff --git a/Tests/LRUCacheTests/LRUCacheTests.swift b/Tests/LRUCacheTests/LRUCacheTests.swift index c423a67..5960fa8 100644 --- a/Tests/LRUCacheTests/LRUCacheTests.swift +++ b/Tests/LRUCacheTests/LRUCacheTests.swift @@ -2,29 +2,16 @@ import Testing @testable import LRUCache import Foundation -struct LRUCacheTests { - @Test - func testCostLimit() { - let cache = LRUCache() - - // Given - cache.totalCostLimit = 10 - - // When - cache[1, cost: 4] = 1 - cache[2, cost: 5] = 2 - cache[3, cost: 5] = 3 - - // Then - #expect(cache[1] == nil) - #expect(cache[2] == 2) - #expect(cache[3] == 3) - } - - @Test - func testCostLimitNSCache() { - let cache = NSCacheWrapper() +private func anyCache(_ actual: some AnyCache) -> any AnyCache { + actual +} +struct LRUCacheTests { + @Test(arguments: [ + anyCache(LRUCacheWrapper()), + anyCache(NSCacheWrapper()), + ]) + func testCostLimit(cache: any AnyCache) { // Given cache.totalCostLimit = 10 @@ -39,10 +26,11 @@ struct LRUCacheTests { #expect(cache[3] == 3) } - @Test - func testCountLimit() { - let cache = LRUCache() - + @Test(arguments: [ + anyCache(LRUCacheWrapper()), + anyCache(NSCacheWrapper()), + ]) + func testCountLimit(cache: any AnyCache) { // Given cache.countLimit = 2 @@ -57,28 +45,11 @@ struct LRUCacheTests { #expect(cache[3] == 3) } - @Test - func testCountLimitNSCache() { - let cache = NSCacheWrapper() - - // Given - cache.countLimit = 2 - - // When - cache[1] = 1 - cache[2] = 2 - cache[3] = 3 - - // Then - #expect(cache[1] == nil) - #expect(cache[2] == 2) - #expect(cache[3] == 3) - } - - @Test - func testCountLimitAccess() { - let cache = LRUCache() - + @Test(arguments: [ + anyCache(LRUCacheWrapper()), + anyCache(NSCacheWrapper()), + ]) + func testCountLimitAccess(cache: any AnyCache) { // Given cache.countLimit = 2 @@ -95,49 +66,11 @@ struct LRUCacheTests { #expect(cache[3] == 3) } - @Test - func testCountLimitAccessNSCache() { - let cache = NSCacheWrapper() - - // Given - cache.countLimit = 2 - - // When - cache[1] = 1 - cache[2] = 2 - - _ = cache[1] - cache[3] = 3 - - // Then - #expect(cache[1] == 1) - #expect(cache[2] == nil) - #expect(cache[3] == 3) - } - - @Test - func testCountLimitWithCost1() { - let cache = LRUCache() - - // Given - cache.countLimit = 2 - cache.totalCostLimit = 5 - - // When - cache[1, cost: 3] = 1 - cache[2, cost: 3] = 2 - cache[3, cost: 3] = 3 - - // Then - #expect(cache[1] == nil) - #expect(cache[2] == nil) - #expect(cache[3] == 3) - } - - @Test - func testCountLimitWithCost1NSCache() { - let cache = NSCacheWrapper() - + @Test(arguments: [ + anyCache(LRUCacheWrapper()), + anyCache(NSCacheWrapper()), + ]) + func testCountLimitWithCost1(cache: any AnyCache) { // Given cache.countLimit = 2 cache.totalCostLimit = 5 @@ -153,29 +86,11 @@ struct LRUCacheTests { #expect(cache[3] == 3) } - @Test - func testCountLimitWithCost2() { - let cache = LRUCache() - - // Given - cache.countLimit = 2 - cache.totalCostLimit = 3 - - // When - cache[1, cost: 3] = 1 - cache[2, cost: 2] = 2 - cache[3, cost: 1] = 3 - - // Then - #expect(cache[1] == nil) - #expect(cache[2] == 2) - #expect(cache[3] == 3) - } - - @Test - func testCountLimitWithCost2NSCache() { - let cache = NSCacheWrapper() - + @Test(arguments: [ + anyCache(LRUCacheWrapper()), + anyCache(NSCacheWrapper()), + ]) + func testCountLimitWithCost2(cache: any AnyCache) { // Given cache.countLimit = 2 cache.totalCostLimit = 3 @@ -191,30 +106,11 @@ struct LRUCacheTests { #expect(cache[3] == 3) } - @Test - func testCountLimitWithCost3() { - let cache = LRUCache() - - // Given - cache.countLimit = 2 - cache.totalCostLimit = 3 - - // When - cache[1, cost: 3] = 1 - cache[2, cost: 2] = 2 - cache[3, cost: 1] = 3 - cache[1, cost: 3] = 1 - - // Then - #expect(cache[1] == 1) - #expect(cache[2] == nil) - #expect(cache[3] == nil) - } - - @Test - func testCountLimitWithCost3NSCache() { - let cache = NSCacheWrapper() - + @Test(arguments: [ + anyCache(LRUCacheWrapper()), + anyCache(NSCacheWrapper()), + ]) + func testCountLimitWithCost3(cache: any AnyCache) { // Given cache.countLimit = 2 cache.totalCostLimit = 3 @@ -231,10 +127,11 @@ struct LRUCacheTests { #expect(cache[3] == nil) } - @Test - func testCountLimitWithCost4() { - let cache = LRUCache() - + @Test(arguments: [ + anyCache(LRUCacheWrapper()), + anyCache(NSCacheWrapper()), + ]) + func testCountLimitWithCost4(cache: any AnyCache) { // Given cache.totalCostLimit = 10 @@ -251,30 +148,11 @@ struct LRUCacheTests { #expect(cache[3] == 3) } - @Test - func testCountLimitWithCost4NSCache() { - let cache = NSCacheWrapper() - - // Given - cache.totalCostLimit = 10 - - // When - cache[1, cost: 3] = 1 - cache[2, cost: 2] = 2 - cache[3, cost: 1] = 3 - cache[1, cost: 3] = 1 - cache[3, cost: 7] = 3 - - // Then - #expect(cache[1] == 1) - #expect(cache[2] == nil) - #expect(cache[3] == 3) - } - - @Test - func testRemoveHeadValue() { - let cache = LRUCache() - + @Test(arguments: [ + anyCache(LRUCacheWrapper()), + anyCache(NSCacheWrapper()), + ]) + func testRemoveHeadValue(cache: any AnyCache) { // Given // - @@ -291,10 +169,11 @@ struct LRUCacheTests { #expect(cache[3] == 3) } - @Test - func testRemoveMiddleValue() { - let cache = LRUCache() - + @Test(arguments: [ + anyCache(LRUCacheWrapper()), + anyCache(NSCacheWrapper()), + ]) + func testRemoveMiddleValue(cache: any AnyCache) { // Given // - @@ -311,10 +190,11 @@ struct LRUCacheTests { #expect(cache[3] == 3) } - @Test - func testRemoveTailValue() { - let cache = LRUCache() - + @Test(arguments: [ + anyCache(LRUCacheWrapper()), + anyCache(NSCacheWrapper()), + ]) + func testRemoveTailValue(cache: any AnyCache) { // Given // - @@ -331,10 +211,11 @@ struct LRUCacheTests { #expect(cache[3] == nil) } - @Test - func testRemoveAll() { - let cache = LRUCache() - + @Test(arguments: [ + anyCache(LRUCacheWrapper()), + anyCache(NSCacheWrapper()), + ]) + func testRemoveAll(cache: any AnyCache) { // Given // - @@ -351,15 +232,16 @@ struct LRUCacheTests { #expect(cache[3] == nil) } - @Test - func testReferenceCount() { - final class MyClass: @unchecked Sendable {} - - let cache = LRUCache() + final class MyClass: @unchecked Sendable {} + @Test(arguments: [ + anyCache(LRUCacheWrapper()), + anyCache(NSCacheWrapper()), + ]) + func testReferenceCount(cache: any AnyCache) { // Given var ref: MyClass? = MyClass() - weak var weakRef = ref + weak let weakRef = ref // When autoreleasepool { @@ -379,26 +261,87 @@ struct LRUCacheTests { } } +protocol AnyCache: Sendable { + associatedtype Key: Hashable + associatedtype Value + + func value(forKey key: Key) -> Value? + func setValue(_ value: Value, forKey key: Key, cost: Int) + func removeValue(forKey key: Key) + func removeAllValues() + + var totalCostLimit: Int { get nonmutating set } + var countLimit: Int { get nonmutating set } + + subscript(_ key: Key, cost cost: Int) -> Value? { get nonmutating set } +} + +extension AnyCache { + func setValue(_ value: Value, forKey key: Key) { + setValue(value, forKey: key, cost: 0) + } + + subscript(_ key: Key, cost cost: Int = 0) -> Value? { + get { + value(forKey: key) + } + nonmutating set { + if let newValue { + setValue(newValue, forKey: key, cost: cost) + } else { + removeValue(forKey: key) + } + } + } +} + // MARK: - private -private final class NSCacheWrapper { - func object(forKey key: Key) -> Object? { - inner.object(forKey: .init(key))?.object +private final class LRUCacheWrapper: AnyCache { + func value(forKey key: Key) -> Value? { + inner.value(forKey: key) } - func setObject(_ obj: Object, forKey key: Key, cost: Int = 0) { - inner.setObject(.init(obj), forKey: .init(key), cost: cost) + func setValue(_ value: Value, forKey key: Key, cost: Int = 0) { + inner.setValue(value, forKey: key, cost: cost) } - func removeObject(forKey key: Key) { - inner.removeObject(forKey: .init(key)) + func removeValue(forKey key: Key) { + inner.removeValue(forKey: key) } - func removeAllObjects() { - inner.removeAllObjects() + func removeAllValues() { + inner.removeAllValues() + } + + var totalCostLimit: Int { + get { inner.totalCostLimit } + set { inner.totalCostLimit = newValue } + } + + var countLimit: Int { + get { inner.countLimit } + set { inner.countLimit = newValue } + } + + // MARK: - + private let inner = LRUCache() +} + +private final class NSCacheWrapper: AnyCache, @unchecked Sendable { + func value(forKey key: Key) -> Value? { + inner.object(forKey: .init(key))?.object + } + + func setValue(_ value: Value, forKey key: Key, cost: Int = 0) { + inner.setObject(.init(value), forKey: .init(key), cost: cost) + } + + func removeValue(forKey key: Key) { + inner.removeObject(forKey: .init(key)) } func removeAllValues() { - removeAllObjects() + inner.removeAllObjects() } var totalCostLimit: Int { @@ -433,25 +376,10 @@ private final class NSCacheWrapper { } private final class ObjectWrapper: NSObject { - let object: Object + let object: Value - init(_ object: Object) { + init(_ object: Value) { self.object = object } } } - -extension NSCacheWrapper { - subscript (_ key: Key, cost cost: Int = 0) -> Object? { - get { - object(forKey: key) - } - set { - if let newValue { - setObject(newValue, forKey: key, cost: cost) - } else { - removeObject(forKey: key) - } - } - } -}