mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-10-28 13:13:47 +00:00
refactor(rules): 删除不可用走 commitFeedback,删分叉枚举与本地 alert
outcome 加 .superseded(事务门拒绝词汇,与 runtime「被取代」一致); deleteCustomRules 改 Void,忙碌/超期发布反馈;CustomRuleDeletionResult 与 RulesDeletionButton 本地 alert 删除——同一确认手势的失败呈现归一。
This commit is contained in:
+1
-1
File diff suppressed because one or more lines are too long
@@ -3,6 +3,7 @@ import Foundation
|
||||
// MARK: - 自定义规则更新结果
|
||||
|
||||
/// The result of accepting and saving a rule document, independent of runtime.
|
||||
/// `busy`/`superseded` are transaction-gate rejections: no save was attempted.
|
||||
enum CustomRuleUpdateOutcome: Equatable, Sendable {
|
||||
/// 规则已保存;运行规则模式时应用。
|
||||
case saved
|
||||
@@ -11,6 +12,8 @@ enum CustomRuleUpdateOutcome: Equatable, Sendable {
|
||||
/// 持久化失败:旧规则保持不变。
|
||||
case persistenceFailed
|
||||
case busy
|
||||
/// 事务门拒绝:意图捕获后事实已前进(集合版本/完整性变化),本次更新未尝试保存。
|
||||
case superseded
|
||||
case invalidDocument(detail: String)
|
||||
|
||||
var isSuccess: Bool {
|
||||
@@ -55,6 +58,7 @@ extension CustomRuleUpdateOutcome {
|
||||
case .saved: RulesCopy.text("已保存")
|
||||
case .persistenceFailed: RulesCopy.text("保存失败,规则未更改")
|
||||
case .busy: RulesCopy.text("正在更新规则…")
|
||||
case .superseded: RulesCopy.text("规则集合已变化,操作未执行。")
|
||||
case .invalidDocument, .rejected: RulesCopy.text("无法更新规则")
|
||||
}
|
||||
}
|
||||
@@ -69,7 +73,7 @@ extension CustomRuleUpdateOutcome {
|
||||
|
||||
var nextStep: String? {
|
||||
switch self {
|
||||
case .persistenceFailed, .busy: "请重试规则操作。"
|
||||
case .persistenceFailed, .busy, .superseded: "请重试规则操作。"
|
||||
case .invalidDocument, .rejected: "请检查规则数据后再操作。"
|
||||
default: nil
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import SwiftUI
|
||||
|
||||
/// A single confirmation captures custom UUIDs before starting the shared transaction.
|
||||
/// Gate rejections and commit failures surface through the workflow's commit
|
||||
/// feedback, not a local alert.
|
||||
struct RulesDeletionButton: View {
|
||||
@ObservedObject var workflow: RulesWorkflow
|
||||
@State private var confirmation: CustomRuleDeletion?
|
||||
@State private var failure: CustomRuleDeletionResult.Failure?
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
@@ -24,35 +25,15 @@ struct RulesDeletionButton: View {
|
||||
) { captured in
|
||||
Button(RulesCopy.text("删除"), role: .destructive) {
|
||||
confirmation = nil
|
||||
Task {
|
||||
if case .unavailable(let reason) = await workflow.deleteCustomRules(captured) {
|
||||
failure = reason
|
||||
}
|
||||
}
|
||||
Task { await workflow.deleteCustomRules(captured) }
|
||||
}
|
||||
Button(RulesCopy.text("取消"), role: .cancel) { confirmation = nil }
|
||||
} message: { _ in
|
||||
Text(RulesCopy.text("只删除所选自定义条目。其他来源的规则和已有禁用记录将保留。"))
|
||||
}
|
||||
.alert(
|
||||
RulesCopy.text("无法更新规则"),
|
||||
isPresented: Binding(get: { failure != nil }, set: { if !$0 { failure = nil } })
|
||||
) {
|
||||
Button(RulesCopy.text("关闭")) { failure = nil }
|
||||
} message: {
|
||||
Text(RulesCopy.text(failureMessage))
|
||||
}
|
||||
}
|
||||
|
||||
private func countText(_ key: String, _ count: Int) -> String {
|
||||
String.localizedStringWithFormat(RulesCopy.text(key), Int64(count))
|
||||
}
|
||||
|
||||
private var failureMessage: String {
|
||||
switch failure {
|
||||
case .staleConfirmation: "规则集合已变化,请重新确认删除。"
|
||||
case .incompleteCollection: "集合不完整"
|
||||
case .busy, nil: "正在更新规则…"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,20 +361,26 @@ extension RulesWorkflow {
|
||||
return CustomRuleDeletion(customIDs: deletableSelection, version: ticket.version)
|
||||
}
|
||||
|
||||
func deleteCustomRules(_ confirmation: CustomRuleDeletion) async -> CustomRuleDeletionResult {
|
||||
guard !snapshot.isCommitting else { return .unavailable(.busy) }
|
||||
guard let ticket = intentTicket(), let old = ticket.document
|
||||
else { return .unavailable(.incompleteCollection) }
|
||||
guard confirmation.version == ticket.version,
|
||||
/// 删除的事务门拒绝(忙碌/超期)与提交失败走同一条 commitFeedback 呈现,
|
||||
/// 不另设返回词汇:确认手势的所有失败口径一致。
|
||||
func deleteCustomRules(_ confirmation: CustomRuleDeletion) async {
|
||||
guard !snapshot.isCommitting else {
|
||||
publishFeedback(.busy, operation: .delete, changedCount: 0, page: snapshot)
|
||||
return
|
||||
}
|
||||
guard let ticket = intentTicket(), let old = ticket.document,
|
||||
!confirmation.customIDs.isEmpty,
|
||||
confirmation.version == ticket.version,
|
||||
confirmation.customIDs.isSubset(of: Set(old.rules.map(\.id)))
|
||||
else { return .unavailable(.staleConfirmation) }
|
||||
else {
|
||||
publishFeedback(.superseded, operation: .delete, changedCount: 0, page: snapshot)
|
||||
return
|
||||
}
|
||||
let document = CustomRuleDocument(
|
||||
rules: old.rules.filter { !confirmation.customIDs.contains($0.id) },
|
||||
disabledIdentities: old.disabledIdentities)
|
||||
return .committed(
|
||||
await commit(
|
||||
document, operation: .delete, changedCount: confirmation.customIDs.count))
|
||||
_ = await commit(
|
||||
document, operation: .delete, changedCount: confirmation.customIDs.count)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
enum CustomRuleDeletionResult: Equatable, Sendable {
|
||||
enum Failure: Equatable, Sendable { case busy, incompleteCollection, staleConfirmation }
|
||||
case unavailable(Failure)
|
||||
case committed(CustomRuleUpdateOutcome)
|
||||
}
|
||||
@@ -31,8 +31,8 @@ final class CustomRuleDeletionControllerTests: ProxyRuntimeControllerTests {
|
||||
await workflow.refresh()
|
||||
workflow.select(Set(workflow.snapshot.rows.map(\.id)))
|
||||
let confirmation = try XCTUnwrap(workflow.prepareCustomRuleDeletion())
|
||||
let result = await workflow.deleteCustomRules(confirmation)
|
||||
XCTAssertEqual(result, .committed(.saved))
|
||||
await workflow.deleteCustomRules(confirmation)
|
||||
XCTAssertEqual(workflow.snapshot.commitOutcome, .saved)
|
||||
await controller.ruleApplicationTask?.value
|
||||
XCTAssertEqual(
|
||||
try store.loadDocument(),
|
||||
@@ -71,8 +71,8 @@ final class CustomRuleDeletionControllerTests: ProxyRuntimeControllerTests {
|
||||
workflow.select(Set(workflow.snapshot.rows.filter { !$0.customIDs.isEmpty }.map(\.id)))
|
||||
let confirmation = try XCTUnwrap(workflow.prepareCustomRuleDeletion())
|
||||
let registrations = agent.registerCount
|
||||
let result = await workflow.deleteCustomRules(confirmation)
|
||||
XCTAssertEqual(result, .committed(.saved))
|
||||
await workflow.deleteCustomRules(confirmation)
|
||||
XCTAssertEqual(workflow.snapshot.commitOutcome, .saved)
|
||||
await controller.ruleApplicationTask?.value
|
||||
XCTAssertEqual(agent.registerCount, registrations + 1)
|
||||
XCTAssertTrue(try store.load().isEmpty)
|
||||
@@ -101,8 +101,8 @@ final class CustomRuleDeletionControllerTests: ProxyRuntimeControllerTests {
|
||||
workflow.select(Set(workflow.snapshot.rows.filter { !$0.customIDs.isEmpty }.map(\.id)))
|
||||
let confirmation = try XCTUnwrap(workflow.prepareCustomRuleDeletion())
|
||||
let registrations = agent.registerCount
|
||||
let result = await workflow.deleteCustomRules(confirmation)
|
||||
XCTAssertEqual(result, .committed(.saved))
|
||||
await workflow.deleteCustomRules(confirmation)
|
||||
XCTAssertEqual(workflow.snapshot.commitOutcome, .saved)
|
||||
await controller.ruleApplicationTask?.value
|
||||
XCTAssertEqual(agent.registerCount, registrations)
|
||||
XCTAssertTrue(try store.load().isEmpty)
|
||||
@@ -135,8 +135,8 @@ final class CustomRuleDeletionControllerTests: ProxyRuntimeControllerTests {
|
||||
try? runtimeStore.writeRuntimeReceipt(
|
||||
for: isRecovery ? requested : previousRuntime, processID: 42)
|
||||
}
|
||||
let result = await workflow.deleteCustomRules(confirmation)
|
||||
XCTAssertEqual(result, .committed(.saved))
|
||||
await workflow.deleteCustomRules(confirmation)
|
||||
XCTAssertEqual(workflow.snapshot.commitOutcome, .saved)
|
||||
let saved = CustomRuleDocument(rules: [], disabledIdentities: [orphan])
|
||||
XCTAssertEqual(try store.loadDocument(), saved)
|
||||
await controller.ruleApplicationTask?.value
|
||||
@@ -172,8 +172,8 @@ final class CustomRuleDeletionControllerTests: ProxyRuntimeControllerTests {
|
||||
try FileManager.default.moveItem(at: directory, to: backupDirectory)
|
||||
try Data("blocks parent directory".utf8).write(to: directory)
|
||||
let backup = backupDirectory.appendingPathComponent(store.fileURL.lastPathComponent)
|
||||
let result = await workflow.deleteCustomRules(confirmation)
|
||||
XCTAssertEqual(result, .committed(.persistenceFailed))
|
||||
await workflow.deleteCustomRules(confirmation)
|
||||
XCTAssertEqual(workflow.snapshot.commitOutcome, .persistenceFailed)
|
||||
XCTAssertEqual(controller.ruleDocuments.current, original)
|
||||
XCTAssertEqual(try Data(contentsOf: backup), originalData)
|
||||
XCTAssertEqual(workflow.snapshot.version, confirmation.version)
|
||||
|
||||
@@ -29,8 +29,8 @@ final class CustomRuleDeletionWorkflowTests: XCTestCase {
|
||||
XCTAssertEqual(confirmation.customIDs, [custom.id, other.id])
|
||||
XCTAssertEqual(commits, 0, "Preparing and discarding a confirmation has no side effects")
|
||||
let oldVersion = workflow.snapshot.version
|
||||
let result = await workflow.deleteCustomRules(confirmation)
|
||||
XCTAssertEqual(result, .committed(.saved))
|
||||
await workflow.deleteCustomRules(confirmation)
|
||||
XCTAssertEqual(workflow.snapshot.commitOutcome, .saved)
|
||||
XCTAssertEqual(commits, 1)
|
||||
XCTAssertTrue(saved.rules.isEmpty)
|
||||
XCTAssertEqual(saved.disabledIdentities, [custom.identity, orphan])
|
||||
@@ -68,11 +68,11 @@ extension CustomRuleDeletionWorkflowTests {
|
||||
query.search = ""
|
||||
workflow.query(query)
|
||||
workflow.select(Set(workflow.snapshot.rows.map(\.id)))
|
||||
let deleted = await workflow.deleteCustomRules(confirmation)
|
||||
XCTAssertEqual(deleted, .committed(.saved))
|
||||
await workflow.deleteCustomRules(confirmation)
|
||||
XCTAssertEqual(workflow.snapshot.commitOutcome, .saved)
|
||||
XCTAssertEqual(saved.rules.map(\.id), [hidden.id], "New selection cannot expand captured UUIDs")
|
||||
let stale = await workflow.deleteCustomRules(confirmation)
|
||||
XCTAssertEqual(stale, .unavailable(.staleConfirmation))
|
||||
await workflow.deleteCustomRules(confirmation)
|
||||
XCTAssertEqual(workflow.snapshot.commitFeedback?.outcome, .superseded)
|
||||
XCTAssertEqual(commits, 1)
|
||||
workflow.select([])
|
||||
XCTAssertNil(workflow.prepareCustomRuleDeletion())
|
||||
@@ -91,8 +91,8 @@ extension CustomRuleDeletionWorkflowTests {
|
||||
workflow.select(Set(workflow.snapshot.rows.map(\.id)))
|
||||
let confirmation = try XCTUnwrap(workflow.prepareCustomRuleDeletion())
|
||||
await workflow.setEnabled(false, identities: [rule.identity])
|
||||
let result = await workflow.deleteCustomRules(confirmation)
|
||||
XCTAssertEqual(result, .unavailable(.staleConfirmation))
|
||||
await workflow.deleteCustomRules(confirmation)
|
||||
XCTAssertEqual(workflow.snapshot.commitFeedback?.outcome, .superseded)
|
||||
XCTAssertEqual(commits, 1)
|
||||
XCTAssertEqual(workflow.deletableSelection, [rule.id])
|
||||
}
|
||||
@@ -121,11 +121,11 @@ extension CustomRuleDeletionWorkflowTests {
|
||||
let pending = Task { await workflow.deleteCustomRules(confirmation) }
|
||||
await fulfillment(of: [started], timeout: 3)
|
||||
XCTAssertNil(workflow.prepareCustomRuleDeletion())
|
||||
let repeated = await workflow.deleteCustomRules(confirmation)
|
||||
XCTAssertEqual(repeated, .unavailable(.busy))
|
||||
await workflow.deleteCustomRules(confirmation)
|
||||
XCTAssertEqual(workflow.snapshot.commitFeedback?.outcome, .busy)
|
||||
continuation?.resume()
|
||||
let result = await pending.value
|
||||
XCTAssertEqual(result, .committed(.persistenceFailed))
|
||||
await pending.value
|
||||
XCTAssertEqual(workflow.snapshot.commitOutcome, .persistenceFailed)
|
||||
XCTAssertEqual(commits, 1)
|
||||
XCTAssertEqual(workflow.snapshot.version, confirmation.version)
|
||||
XCTAssertEqual(workflow.prepareCustomRuleDeletion(), confirmation)
|
||||
@@ -150,8 +150,8 @@ extension CustomRuleDeletionWorkflowTests {
|
||||
failed = true
|
||||
await workflow.refresh()
|
||||
XCTAssertNil(workflow.prepareCustomRuleDeletion())
|
||||
let result = await workflow.deleteCustomRules(confirmation)
|
||||
XCTAssertEqual(result, .unavailable(.incompleteCollection))
|
||||
await workflow.deleteCustomRules(confirmation)
|
||||
XCTAssertEqual(workflow.snapshot.commitFeedback?.outcome, .superseded)
|
||||
XCTAssertEqual(commits, 0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,10 @@ final class RulesPresentationTests: XCTestCase {
|
||||
CustomRuleUpdateOutcome.persistenceFailed.nextStep, RulesCopy.text("请重试规则操作。"))
|
||||
XCTAssertNil(CustomRuleUpdateOutcome.persistenceFailed.failureDetail)
|
||||
XCTAssertEqual(CustomRuleUpdateOutcome.busy.rulesMessage, RulesCopy.text("正在更新规则…"))
|
||||
XCTAssertEqual(
|
||||
CustomRuleUpdateOutcome.superseded.rulesMessage, RulesCopy.text("规则集合已变化,操作未执行。"))
|
||||
XCTAssertEqual(CustomRuleUpdateOutcome.superseded.nextStep, RulesCopy.text("请重试规则操作。"))
|
||||
XCTAssertNil(CustomRuleUpdateOutcome.superseded.failureDetail)
|
||||
let invalid = CustomRuleUpdateOutcome.invalidDocument(detail: "detail-1")
|
||||
XCTAssertEqual(invalid.rulesMessage, RulesCopy.text("无法更新规则"))
|
||||
XCTAssertEqual(invalid.failureDetail, "detail-1")
|
||||
|
||||
Reference in New Issue
Block a user