refactor(history): share local recents projection

This commit is contained in:
Kaede Akino committed 2026-09-25 19:52:30 +08:00
1 parent a728b48df2
commit 78070dcdcd
9 files changed
+98 -42

No files matched your search

@@ -0,0 +1,50 @@
import Foundation
import TermindApplication
import TermindDomain
import Testing
import struct TermindDomain.Host
@Suite @MainActor
struct RecentConnectionHostTests {
@Test func projectionKeepsTheNewestLocalEventForEachSavedHost() {
let localID = UUID()
let first = Host(name: "First", host: "first.example")
let second = Host(name: "Second", host: "second.example")
let removedID = UUID()
let firstOlder = ConnectionHistoryEntry(
hostID: first.id, kind: .ssh,
connectedAt: Date(timeIntervalSince1970: 10), sourceDeviceID: localID)
let secondLatest = ConnectionHistoryEntry(
hostID: second.id, kind: .telnet,
connectedAt: Date(timeIntervalSince1970: 30), sourceDeviceID: localID)
let firstLatest = ConnectionHistoryEntry(
hostID: first.id, kind: .sftp,
connectedAt: Date(timeIntervalSince1970: 20), sourceDeviceID: localID)
let history = ConnectionHistoryModel(
entries: [
firstOlder,
ConnectionHistoryEntry(
hostID: first.id, kind: .ssh,
connectedAt: Date(timeIntervalSince1970: 50), sourceDeviceID: UUID()),
secondLatest,
ConnectionHistoryEntry(
hostID: removedID, kind: .ssh,
connectedAt: Date(timeIntervalSince1970: 40), sourceDeviceID: localID),
firstLatest,
ConnectionHistoryEntry(
hostID: second.id, kind: .ssh,
connectedAt: Date(timeIntervalSince1970: 60)),
],
sourceDeviceID: localID)
let library = HostLibraryModel(hosts: [first, second])
let all = history.recentLocalHosts(in: library, limit: 5)
#expect(all.map(\.id) == [second.id, first.id])
#expect(all.map(\.entry.id) == [secondLatest.id, firstLatest.id])
#expect(all.map(\.host.name) == ["Second", "First"])
#expect(history.recentLocalHosts(in: library, limit: 1).map(\.id) == [second.id])
#expect(history.recentLocalHosts(in: library, limit: 0).isEmpty)
#expect(history.recentLocalHosts(in: library, limit: -1).isEmpty)
}
}
@@ -4,6 +4,13 @@ import TermindDomain
import struct TermindDomain.Host
public struct RecentConnectionHost: Identifiable {
public let entry: ConnectionHistoryEntry
public let host: Host
public var id: Host.ID { host.id }
}
@MainActor
@Observable
public final class ConnectionHistoryModel {
@@ -55,6 +62,20 @@ public final class ConnectionHistoryModel {
entries().filter(isLocal)
}
/// The latest local connection for each saved Host, in connection order.
public func recentLocalHosts(in library: HostLibraryModel, limit: Int) -> [RecentConnectionHost] {
guard limit > 0 else { return [] }
var seen = Set<Host.ID>()
var hosts: [RecentConnectionHost] = []
for entry in localEntries() {
guard let host = library.host(entry.hostID), seen.insert(entry.hostID).inserted else { continue }
hosts.append(RecentConnectionHost(entry: entry, host: host))
if hosts.count == limit { break }
}
return hosts
}
public func mostRecentDate(for hostID: Host.ID) -> Date? {
entries().first { $0.hostID == hostID }?.connectedAt
}
@@ -49,28 +49,20 @@ enum MobileConnectionHistoryActionPolicy {
@MainActor
enum MobileConnectionHistory {
static func recentHostRows(
entries: [ConnectionHistoryEntry],
history: ConnectionHistoryModel,
library: HostLibraryModel,
limit: Int
) -> [MobileRecentHostRow] {
guard limit > 0 else { return [] }
var seen = Set<Host.ID>()
var rows: [MobileRecentHostRow] = []
for entry in entries {
guard let host = library.host(entry.hostID) else { continue }
guard seen.insert(host.id).inserted else { continue }
history.recentLocalHosts(in: library, limit: limit).map { recent in
let host = recent.host
let preferredKind = preferredKind(for: host)
rows.append(
MobileRecentHostRow(
entry: entry,
host: host,
loginAddress: library.preferredTerminalLoginAddress(for: host),
preferredKind: preferredKind
))
if rows.count == limit { break }
return MobileRecentHostRow(
entry: recent.entry,
host: host,
loginAddress: library.preferredTerminalLoginAddress(for: host),
preferredKind: preferredKind
)
}
return rows
}
static func eventRows(
@@ -187,7 +187,7 @@ struct MobileWorkspaceListView: View {
var historyRows: [MobileRecentHostRow] {
_ = app.connectionHistory.revision
return MobileConnectionHistory.recentHostRows(
entries: app.connectionHistory.localEntries(),
history: app.connectionHistory,
library: app.library,
limit: Self.historyLimit
)
@@ -77,7 +77,7 @@ enum MobileHomeScreenQuickActions {
static func refresh(app: MobileAppModel) {
let rows = MobileConnectionHistory.recentHostRows(
entries: app.connectionHistory.localEntries(),
history: app.connectionHistory,
library: app.library,
limit: ConnectionHistoryModel.limit
)
@@ -37,7 +37,7 @@ struct MobileHomeScreenQuickActionTests {
kind: .ssh, for: UUID(), at: Date(timeIntervalSince1970: 70))
let rows = MobileConnectionHistory.recentHostRows(
entries: app.connectionHistory.localEntries(),
history: app.connectionHistory,
library: app.library,
limit: ConnectionHistoryModel.limit
)
-11
View File
@@ -980,17 +980,6 @@ final class AppModel {
connectionHistory.clearAll()
}
/// Saved Hosts ordered by their latest connection event, with each Host
/// appearing only once.
func recentHosts(limit: Int = 5, localOnly: Bool = false) -> [Host] {
var seen = Set<Host.ID>()
let entries = localOnly ? connectionHistory.localEntries() : connectionHistory.entries()
return entries.compactMap { entry in
guard seen.insert(entry.hostID).inserted else { return nil }
return hosts.host(entry.hostID)
}.prefix(limit).map { $0 }
}
func lastConnectedAt(for hostID: Host.ID) -> Date? {
connectionHistory.mostRecentDate(for: hostID)
}
@@ -989,11 +989,12 @@ struct WelcomeView: View {
}
}
}
let hosts = app.recentHosts(limit: max(0, 6 - templates.count), localOnly: true)
let hosts = app.connectionHistory.recentLocalHosts(
in: app.hostLibrary, limit: max(0, 6 - templates.count))
if !hosts.isEmpty {
section(AppStrings.Workspace.recents) {
ForEach(hosts) { host in
WelcomeHostRow(host: host)
ForEach(hosts) { recent in
WelcomeHostRow(host: recent.host)
}
}
}
+11 -8
View File
@@ -246,7 +246,7 @@ struct ConnectionHistoryTests {
#expect(history.entries().map(\.hostID) == [first.id, second.id, first.id])
#expect(history.entries().map(\.kind) == [.sftp, .sftp, .ssh])
#expect(app.recentHosts().map(\.id) == [first.id, second.id])
#expect(history.recentLocalHosts(in: app.hostLibrary, limit: 5).map(\.id) == [first.id, second.id])
#expect(app.lastConnectedAt(for: first.id) != nil)
}
@@ -270,8 +270,8 @@ struct ConnectionHistoryTests {
let app = AppModel(
hosts: HostLibraryModel(hosts: [local, remote]), connectionHistory: history)
#expect(app.recentHosts(localOnly: true).map(\.id) == [local.id])
#expect(app.recentHosts(localOnly: false).map(\.id) == [remote.id, local.id])
#expect(history.recentLocalHosts(in: app.hostLibrary, limit: 5).map(\.id) == [local.id])
#expect(history.entries().map(\.hostID) == [remote.id, local.id])
#expect(app.lastConnectedLocallyAt(for: local.id) == Date(timeIntervalSince1970: 10))
#expect(app.lastConnectedLocallyAt(for: remote.id) == nil)
}
@@ -280,10 +280,13 @@ struct ConnectionHistoryTests {
func clearAllHistoryForHostKeepsHostAndOtherHostHistory() {
let host = Host(name: "Host", host: "host.example")
let other = Host(name: "Other", host: "other.example")
let connections = ConnectionHistoryModel(entries: [
ConnectionHistoryEntry(hostID: host.id, kind: .ssh),
ConnectionHistoryEntry(hostID: other.id, kind: .ssh),
])
let localID = UUID()
let connections = ConnectionHistoryModel(
entries: [
ConnectionHistoryEntry(hostID: host.id, kind: .ssh, sourceDeviceID: localID),
ConnectionHistoryEntry(hostID: other.id, kind: .ssh, sourceDeviceID: localID),
],
sourceDeviceID: localID)
let commands = CommandHistoryModel(entries: [
CommandHistoryEntry(hostID: host.id, command: "uptime")
])
@@ -303,6 +306,6 @@ struct ConnectionHistoryTests {
#expect(commands.entries(for: host.id).isEmpty)
#expect(files.entries(for: host.id).isEmpty)
#expect(connections.entries(for: other.id).count == 1)
#expect(app.recentHosts().map(\.id) == [other.id])
#expect(connections.recentLocalHosts(in: app.hostLibrary, limit: 5).map(\.id) == [other.id])
}
}