Skip to content

Commit 5bcedfc

Browse files
authored
Merge pull request Dimillian#6 from lRoMYl/feature/simple-cache
Minor fetch performance update with URLCache or NSCache
2 parents 764501b + 6dde62e commit 5bcedfc

File tree

6 files changed

+98
-13
lines changed

6 files changed

+98
-13
lines changed

Sources/CodexSkillManager/Skills/Remote/RemoteSkillClient.swift

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,20 @@ struct RemoteSkillClient {
99
}
1010

1111
extension RemoteSkillClient {
12+
// Static URLSession configured with URLCache (10MB memory, 50MB disk)
13+
// Shared across all client instances for efficiency
14+
private static let session: URLSession = {
15+
let urlCache = URLCache(
16+
memoryCapacity: 10 * 1024 * 1024,
17+
diskCapacity: 50 * 1024 * 1024
18+
)
19+
let config = URLSessionConfiguration.default
20+
config.urlCache = urlCache
21+
return URLSession(configuration: config)
22+
}()
23+
1224
static func live(baseURL: URL = URL(string: "https://clawdhub.com")!) -> RemoteSkillClient {
13-
let session = URLSession.shared
25+
let session = Self.session
1426

1527
return RemoteSkillClient(
1628
fetchLatest: { limit in
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import Foundation
2+
3+
/// Cached detail data for a remote skill
4+
struct CachedSkillDetail {
5+
let markdown: String
6+
let owner: RemoteSkillOwner?
7+
}
8+
9+
/// Memory cache for remote skill details using NSCache
10+
/// Provides automatic memory pressure eviction
11+
final class RemoteSkillDetailCache: @unchecked Sendable {
12+
static let shared = RemoteSkillDetailCache()
13+
14+
private let cache = NSCache<NSString, CacheEntry>()
15+
16+
/// Wrapper class since NSCache requires reference types
17+
private final class CacheEntry {
18+
let detail: CachedSkillDetail
19+
init(_ detail: CachedSkillDetail) { self.detail = detail }
20+
}
21+
22+
private init() {
23+
cache.countLimit = 50
24+
}
25+
26+
func get(slug: String, version: String?) -> CachedSkillDetail? {
27+
let key = cacheKey(slug: slug, version: version)
28+
return cache.object(forKey: key)?.detail
29+
}
30+
31+
func set(_ detail: CachedSkillDetail, slug: String, version: String?) {
32+
let key = cacheKey(slug: slug, version: version)
33+
cache.setObject(CacheEntry(detail), forKey: key)
34+
}
35+
36+
private func cacheKey(slug: String, version: String?) -> NSString {
37+
"\(slug):\(version ?? "latest")" as NSString
38+
}
39+
}

Sources/CodexSkillManager/Skills/Remote/RemoteSkillDetailView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ struct RemoteSkillDetailView: View {
1212
loadingView(for: skill)
1313
case .failed(let message):
1414
errorView(for: skill, message: message)
15-
case .loaded:
15+
case .loaded, .cachedRefreshing:
1616
markdownView(for: skill)
1717
}
1818
}

Sources/CodexSkillManager/Skills/Remote/RemoteSkillStore.swift

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import Observation
1414
case idle
1515
case loading
1616
case loaded
17+
case cachedRefreshing
1718
case failed(String)
1819
}
1920

@@ -28,6 +29,7 @@ import Observation
2829

2930
private let apiClient: RemoteSkillClient
3031
private let fileWorker = SkillFileWorker()
32+
private let detailCache = RemoteSkillDetailCache.shared
3133
private var activeSearchToken = 0
3234
private var activeSearchQuery = ""
3335

@@ -86,18 +88,45 @@ import Observation
8688
return
8789
}
8890

89-
detailState = .loading
90-
detailOwner = nil
91+
// Check NSCache first (application-level cache)
92+
if let cached = detailCache.get(slug: skill.slug, version: skill.latestVersion) {
93+
detailMarkdown = cached.markdown
94+
detailOwner = cached.owner
95+
detailState = .cachedRefreshing
96+
} else {
97+
detailState = .loading
98+
detailMarkdown = ""
99+
detailOwner = nil
100+
}
91101

102+
// Fetch from network (URLCache may provide HTTP-level caching)
92103
do {
93-
detailOwner = try await apiClient.fetchDetail(skill.slug)
104+
let owner = try await apiClient.fetchDetail(skill.slug)
94105
let zipURL = try await apiClient.download(skill.slug, skill.latestVersion)
95-
let markdown = try await fileWorker.loadRawMarkdown(from: zipURL)
96-
detailMarkdown = stripFrontmatter(from: markdown)
106+
let markdown = stripFrontmatter(from: try await fileWorker.loadRawMarkdown(from: zipURL))
107+
108+
guard skill.id == selectedSkillID else { return }
109+
110+
// Update NSCache with fresh data
111+
detailCache.set(
112+
CachedSkillDetail(markdown: markdown, owner: owner),
113+
slug: skill.slug,
114+
version: skill.latestVersion
115+
)
116+
117+
detailOwner = owner
118+
detailMarkdown = markdown
97119
detailState = .loaded
98120
} catch {
99-
detailState = .failed(error.localizedDescription)
100-
detailMarkdown = ""
121+
guard skill.id == selectedSkillID else { return }
122+
123+
// If we had cached content, silently keep showing it
124+
if detailState == .cachedRefreshing {
125+
detailState = .loaded
126+
} else {
127+
detailState = .failed(error.localizedDescription)
128+
detailMarkdown = ""
129+
}
101130
}
102131
}
103132

Sources/CodexSkillManager/Skills/Sidebar/SkillListView.swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import SwiftUI
22

33
struct SkillListView: View {
44
@Environment(SkillStore.self) private var store
5+
@Environment(RemoteSkillStore.self) private var remoteStore
56

67
let localSkills: [Skill]
78
let remoteLatestSkills: [RemoteSkill]
@@ -55,12 +56,18 @@ struct SkillListView: View {
5556
.toolbar {
5657
ToolbarItem(placement: .primaryAction) {
5758
Button {
58-
Task { await store.loadSkills() }
59+
Task {
60+
switch source {
61+
case .local:
62+
await store.loadSkills()
63+
case .clawdhub:
64+
await remoteStore.loadLatest()
65+
}
66+
}
5967
} label: {
6068
Label("Reload", systemImage: "arrow.clockwise")
6169
}
6270
.labelStyle(.iconOnly)
63-
.disabled(source != .local)
6471
}
6572
}
6673
}

Sources/CodexSkillManager/Skills/SkillSplitView.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -338,8 +338,6 @@ private struct SkillSplitLifecycleModifier: ViewModifier {
338338
Task { await store.loadSelectedSkill() }
339339
searchTask?.cancel()
340340
searchTask = nil
341-
} else {
342-
Task { await remoteStore.loadLatest() }
343341
}
344342
}
345343
.onChange(of: searchText) { _, newValue in

0 commit comments

Comments
 (0)