fix(connectivity): remove automatic result expiry

This commit is contained in:
Qiu Yuzhou
2026-10-31 18:07:23 +08:00
parent 27d455f52e
commit 5e55adfc8e
7 changed files with 52 additions and 86 deletions
+2 -2
View File
@@ -90,10 +90,10 @@ _避免用词_:可移除节点(Removable node)。
服务器连通性测试请求访问的目标地址,是用户可修改的非敏感偏好信息,在展示、日志与诊断中无需脱敏。
**代理响应延迟(Proxy response latency)**:
完成目标地址选择与预热后,经指定服务器配置访问测试 URL 的 HTTP 响应头耗时,以成功样本的中位数表示。它与测试目标及测试条件有关,不表示服务器 IP 的纯网络往返时间或下载速度。
完成目标地址选择与预热后,经指定服务器配置访问测试 URL 的 HTTP 响应头耗时,以成功样本的中位数表示。结果保留测试时间,不按固定时长或网络变化自动判为过期,用户结合测试时间自行判断参考价值。它与测试目标及测试条件有关,不表示服务器 IP 的纯网络往返时间或下载速度。
**配置组最快响应延迟(Fastest group response latency)**:
配置组全部后代服务器的成功测试结果中,符合展示条件的最小代理响应延迟。它不表示活动配置组实际选用的服务器或负载均衡后的响应耗时。
配置组全部后代服务器最近一次测试结果中的成功项,其最小代理响应延迟。最近一次结果失败的服务器不贡献更早的成功结果;汇总不按测试时间排除结果。它不表示活动配置组实际选用的服务器或负载均衡后的响应耗时。
**托管插件(Managed plugin)**:
由产品随应用发布的 SIP003 插件,其发布版本与来源事实属于随应用提供的程序,不代表用户自备的同名程序。
@@ -14,10 +14,6 @@ final class ServerConnectivity: ObservableObject {
let id = UUID()
let finishedAt: Date
let outcome: Outcome
var networkExpired = false
func isExpired(at date: Date) -> Bool {
networkExpired || date.timeIntervalSince(finishedAt) >= 300
}
}
struct Batch {
let targets: [NodeID]
@@ -116,15 +112,13 @@ final class ServerConnectivity: ObservableObject {
}
}
func summary(for ids: [NodeID], at date: Date) -> Summary {
func summary(for ids: [NodeID]) -> Summary {
let values = ids.compactMap { id in records[id]?.first.map { (id, $0) } }
let successes = values.filter {
if case .success = $0.1.outcome { return true }
return false
}
let fresh = successes.filter { !$0.1.isExpired(at: date) }
let candidates = fresh.isEmpty ? successes : fresh
let fastest = candidates.min { lhs, rhs in
let fastest = successes.min { lhs, rhs in
guard case .success(let leftMilliseconds) = lhs.1.outcome,
case .success(let rightMilliseconds) = rhs.1.outcome
else {
@@ -152,13 +146,6 @@ final class ServerConnectivity: ObservableObject {
func networkChanged() {
generation += 1
for id in records.keys {
records[id] = records[id]?.map { record in
var copy = record
copy.networkExpired = true
return copy
}
}
for id in testing { jobs[id]?.cancel() }
}
@@ -52,23 +52,6 @@
}
}
},
"已过期": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Expired"
}
},
"zh-Hans": {
"stringUnit": {
"state": "translated",
"value": "已过期"
}
}
}
},
"无成功结果": {
"extractionState": "manual",
"localizations": {
@@ -159,12 +159,10 @@ private struct ObservedConnectivityProperties<Content: View>: View {
var body: some View {
if node.isGroup {
TimelineView(.periodic(from: .now, by: 1)) { context in
content(
NodeDetailPresentation.connectivitySummary(
module.summary(for: node.connectivityServerIDs, at: context.date),
serverName: serverName, locale: locale, timeZone: timeZone))
}
content(
NodeDetailPresentation.connectivitySummary(
module.summary(for: node.connectivityServerIDs),
serverName: serverName, locale: locale, timeZone: timeZone))
} else {
content(
NodeDetailPresentation.connectivityHistory(
@@ -1,6 +1,6 @@
import SwiftUI
/// Observes only the network observations, with a periodic expiry projection.
/// Observes the latest connectivity measurements and testing progress.
struct ConnectivityCell: View {
@ObservedObject var module: ServerConnectivity
let node: CatalogTreeNode
@@ -8,43 +8,38 @@ struct ConnectivityCell: View {
var selected = false
var body: some View {
TimelineView(.periodic(from: .now, by: 1)) { context in
let value = presentation(at: context.date)
HStack(spacing: 5) {
Text(value.text)
.foregroundStyle(
selected
? AnyShapeStyle(
Color(nsColor: .alternateSelectedControlTextColor)
.opacity(value.expired ? 0.85 : 1))
: AnyShapeStyle(value.expired ? .secondary : .primary))
if value.testing {
ProgressView().controlSize(.mini)
.tint(selected ? Color(nsColor: .alternateSelectedControlTextColor) : nil)
}
let value = presentation()
HStack(spacing: 5) {
Text(value.text)
.foregroundStyle(
selected
? AnyShapeStyle(Color(nsColor: .alternateSelectedControlTextColor))
: AnyShapeStyle(.primary))
if value.testing {
ProgressView().controlSize(.mini)
.tint(selected ? Color(nsColor: .alternateSelectedControlTextColor) : nil)
}
.help(value.help)
.accessibilityElement(children: .ignore)
.accessibilityLabel(Text(ConnectivityCopy.text("响应延迟")))
.accessibilityValue(
Text(
value.text + " " + value.help + (value.testing ? " " + ConnectivityCopy.text("测试中") : ""))
)
}
.help(value.help)
.accessibilityElement(children: .ignore)
.accessibilityLabel(Text(ConnectivityCopy.text("响应延迟")))
.accessibilityValue(
Text(
value.text + " " + value.help + (value.testing ? " " + ConnectivityCopy.text("测试中") : ""))
)
}
private struct Presentation {
let text: String
let help: String
let expired: Bool
let testing: Bool
}
private func presentation(at date: Date) -> Presentation {
private func presentation() -> Presentation {
let record: ServerConnectivity.Record?
let text: String
var prefix = ""
let summary = module.summary(for: node.connectivityServerIDs, at: date)
let summary = module.summary(for: node.connectivityServerIDs)
if node.isGroup {
record = summary.record
text = record.map { ConnectivityCopy.outcome($0.outcome) } ?? (summary.hasResults ? "—" : "")
@@ -53,15 +48,13 @@ struct ConnectivityCell: View {
record = module.history(for: node.id).first
text = record.map { ConnectivityCopy.outcome($0.outcome) } ?? ""
}
let expired = record?.isExpired(at: date) ?? false
let help =
record.map {
prefix + $0.finishedAt.formatted(date: .numeric, time: .standard)
+ (expired ? " · " + ConnectivityCopy.text("已过期") : "")
}
?? (summary.hasResults && node.isGroup
? ConnectivityCopy.text("无成功结果") : ConnectivityCopy.text("尚未测试"))
return Presentation(text: text, help: help, expired: expired, testing: summary.isTesting)
return Presentation(text: text, help: help, testing: summary.isTesting)
}
}
@@ -97,13 +97,13 @@ struct ServerConnectivityTests {
}
#expect(module.history(for: id).count == 10)
#expect(module.history(for: id).first?.outcome == .http(302))
let summary = module.summary(for: [id], at: now)
let summary = module.summary(for: [id])
#expect(summary.hasResults)
#expect(summary.record == nil)
#expect(!summary.isTesting)
}
@Test func freshResultsBeatStaleAndTiesUseCatalogOrder() async {
@Test func fastestLatestSuccessIgnoresAgeAndTiesUseCatalogOrder() async {
let ids = (0..<3).map { _ in NodeID.fresh() }
var now = Date(timeIntervalSince1970: 100)
var latency = 1
@@ -114,12 +114,14 @@ struct ServerConnectivityTests {
now.addTimeInterval(300)
latency = 20
await module.wait(for: module.test(Array(ids.suffix(2))))
#expect(module.summary(for: ids, at: now).serverID == ids[1])
let firstRecord = module.history(for: ids[0]).first
#expect(module.summary(for: ids).serverID == ids[0])
#expect(module.summary(for: Array(ids.suffix(2))).serverID == ids[1])
module.networkChanged()
#expect(module.summary(for: ids, at: now).serverID == ids[0])
#expect(module.summary(for: ids, at: now).record?.isExpired(at: now) == true)
#expect(module.summary(for: ids).serverID == ids[0])
#expect(module.history(for: ids[0]).first == firstRecord)
module.setURL(URL(string: "https://example.com/new")!)
#expect(!module.summary(for: ids, at: now).hasResults)
#expect(!module.summary(for: ids).hasResults)
}
@Test func urlAndNetworkChangesSuppressInFlightAndShutdownDrainsQueuedWork() async {
@@ -118,7 +118,7 @@ struct NodeDetailPresentationTests {
let date = Date(timeIntervalSince1970: 37_421)
let records = [
ServerConnectivity.Record(
finishedAt: date, outcome: .success(milliseconds: 42), networkExpired: true),
finishedAt: date, outcome: .success(milliseconds: 42)),
ServerConnectivity.Record(finishedAt: date, outcome: .requestTimeout),
ServerConnectivity.Record(finishedAt: date.addingTimeInterval(-60), outcome: .http(302)),
]
@@ -158,7 +158,7 @@ struct NodeDetailPresentationTests {
}
@Test
func connectivityGroupPropertiesFollowFreshnessNetworkChangesAndNames() async {
func connectivityGroupPropertiesFollowLatestResultsNetworkChangesAndNames() async {
let ids = [NodeID.fresh(), NodeID.fresh()]
var now = Date(timeIntervalSince1970: 37_421)
var latency = 10
@@ -170,28 +170,31 @@ struct NodeDetailPresentationTests {
method: "aes-256-gcm", plugin: nil, pluginOpts: nil)
}, now: { now },
measure: { _, _, _ in .success(milliseconds: latency) })
func properties(at date: Date) -> NodeDetailPresentation.Properties {
func properties() -> NodeDetailPresentation.Properties {
NodeDetailPresentation.connectivitySummary(
module.summary(for: ids, at: date), serverName: { $0 == ids[0] ? name : "Second" },
module.summary(for: ids), serverName: { $0 == ids[0] ? name : "Second" },
locale: Locale(identifier: "en_GB"), timeZone: TimeZone(secondsFromGMT: 0)!)
}
await module.wait(for: module.test([ids[0]]))
#expect(properties(at: now).items.map(\.value) == ["10 ms", "First", "10:23:41"])
#expect(properties().items.map(\.value) == ["10 ms", "First", "10:23:41"])
name = "Renamed"
#expect(properties(at: now).items[1].value == "Renamed")
#expect(properties().items[1].value == "Renamed")
now.addTimeInterval(60)
latency = 20
await module.wait(for: module.test([ids[1]]))
#expect(properties(at: now).items[1].value == "Renamed")
// First expires while Second is still fresh, without another measurement.
#expect(properties().items[1].value == "Renamed")
// The older result remains the fastest without another measurement.
#expect(
properties(at: now.addingTimeInterval(240)).items.map(\.value) == [
"20 ms", "Second", "10:24:41",
properties().items.map(\.value) == [
"10 ms", "Renamed", "10:23:41",
])
module.networkChanged()
#expect(properties(at: now.addingTimeInterval(240)).items[1].value == "Renamed")
#expect(
properties().items.map(\.value) == [
"10 ms", "Renamed", "10:23:41",
])
module.setURL(URL(string: "http://example.com/check")!)
#expect(properties(at: now).items.map(\.value) == [ConnectivityCopy.text("尚未测试")])
#expect(properties().items.map(\.value) == [ConnectivityCopy.text("尚未测试")])
}
private func containsFooter(in view: NSView) -> Bool {