From 25c54a4c1db41dbdca1bdb9f7e709a1c69688eb0 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Fri, 13 Dec 2024 08:57:19 -0800 Subject: [PATCH] Add Core module with Cache. --- Core/Package.swift | 21 +++++++ Core/Sources/Core/Cache.swift | 85 +++++++++++++++++++++++++++ Core/Tests/CoreTests/CoreTests.swift | 12 ++++ NetNewsWire.xcodeproj/project.pbxproj | 2 + 4 files changed, 120 insertions(+) create mode 100644 Core/Package.swift create mode 100644 Core/Sources/Core/Cache.swift create mode 100644 Core/Tests/CoreTests/CoreTests.swift diff --git a/Core/Package.swift b/Core/Package.swift new file mode 100644 index 000000000..e1882a15c --- /dev/null +++ b/Core/Package.swift @@ -0,0 +1,21 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +let package = Package( + name: "Core", + platforms: [.macOS(.v13), .iOS(.v16)], + products: [ + .library( + name: "Core", + targets: ["Core"]), + ], + targets: [ + .target( + name: "Core"), + .testTarget( + name: "CoreTests", + dependencies: ["Core"] + ), + ] +) diff --git a/Core/Sources/Core/Cache.swift b/Core/Sources/Core/Cache.swift new file mode 100644 index 000000000..33ee208b5 --- /dev/null +++ b/Core/Sources/Core/Cache.swift @@ -0,0 +1,85 @@ +// +// Cache.swift +// +// +// Created by Brent Simmons on 10/12/24. +// + +import Foundation +import os + +public protocol CacheRecord: Sendable { + var dateCreated: Date { get } +} + +public final class Cache: Sendable { + + public let timeToLive: TimeInterval + public let timeBetweenCleanups: TimeInterval + + private struct State: Sendable { + var lastCleanupDate = Date() + var cache = [String: T]() + } + + private let stateLock = OSAllocatedUnfairLock(initialState: State()) + + public init(timeToLive: TimeInterval, timeBetweenCleanups: TimeInterval) { + self.timeToLive = timeToLive + self.timeBetweenCleanups = timeBetweenCleanups + } + + public subscript(_ key: String) -> T? { + get { + stateLock.withLock { state in + + cleanupIfNeeded(&state) + + guard let value = state.cache[key] else { + return nil + } + if value.dateCreated.timeIntervalSinceNow < -timeToLive { + state.cache[key] = nil + return nil + } + + return value + } + } + set { + stateLock.withLock { state in + state.cache[key] = newValue + } + } + } + + public func cleanup() { + stateLock.withLock { state in + cleanupIfNeeded(&state) + } + } +} + +extension Cache { + + private func cleanupIfNeeded(_ state: inout State) { + + let currentDate = Date() + guard state.lastCleanupDate.timeIntervalSince(currentDate) < -timeBetweenCleanups else { + return + } + + var keysToDelete = [String]() + for (key, value) in state.cache { + if value.dateCreated.timeIntervalSince(currentDate) < -timeToLive { + keysToDelete.append(key) + } + } + + for key in keysToDelete { + state.cache[key] = nil + } + + state.lastCleanupDate = Date() + } +} diff --git a/Core/Tests/CoreTests/CoreTests.swift b/Core/Tests/CoreTests/CoreTests.swift new file mode 100644 index 000000000..3cad2b63f --- /dev/null +++ b/Core/Tests/CoreTests/CoreTests.swift @@ -0,0 +1,12 @@ +import XCTest +@testable import Core + +final class CoreTests: XCTestCase { + func testExample() throws { + // XCTest Documentation + // https://developer.apple.com/documentation/xctest + + // Defining Test Cases and Test Methods + // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods + } +} diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 942a40d95..9f7020527 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -1029,6 +1029,7 @@ 8405DDA122168920008CE1BF /* TimelineTableView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TimelineTableView.xib; sourceTree = ""; }; 8405DDA422168C62008CE1BF /* TimelineContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineContainerViewController.swift; sourceTree = ""; }; 840BEE4021D70E64009BBAFA /* CrashReportWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashReportWindowController.swift; sourceTree = ""; }; + 840C544F2D0C9A4A00A240DB /* Core */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Core; sourceTree = ""; }; 840D617C2029031C009BC708 /* NetNewsWire.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = NetNewsWire.app; sourceTree = BUILT_PRODUCTS_DIR; }; 840D617E2029031C009BC708 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 840D61952029031D009BC708 /* NetNewsWire_iOSTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetNewsWire_iOSTests.swift; sourceTree = ""; }; @@ -2028,6 +2029,7 @@ 51CD32C724D2E06C009ABAEF /* Secrets */, 51CD32A824D2CB25009ABAEF /* SyncDatabase */, 843E2F152CF2B43700ED170F /* RSWeb */, + 840C544F2D0C9A4A00A240DB /* Core */, ); sourceTree = ""; usesTabs = 1;