mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-01-29 08:45:41 +08:00
373 lines
13 KiB
Rust
373 lines
13 KiB
Rust
use parking_lot::RwLock;
|
||
use std::sync::Arc;
|
||
|
||
pub type SharedBox<T> = Arc<Box<T>>;
|
||
type DraftInner<T> = (SharedBox<T>, Option<SharedBox<T>>);
|
||
|
||
/// Draft 管理:committed 与 optional draft 都以 Arc<Box<T>> 存储,
|
||
// (committed_snapshot, optional_draft_snapshot)
|
||
#[derive(Debug, Clone)]
|
||
pub struct Draft<T: Clone> {
|
||
inner: Arc<RwLock<DraftInner<T>>>,
|
||
}
|
||
|
||
impl<T: Clone> Draft<T> {
|
||
#[inline]
|
||
pub fn new(data: T) -> Self {
|
||
Self {
|
||
inner: Arc::new(RwLock::new((Arc::new(Box::new(data)), None))),
|
||
}
|
||
}
|
||
/// 以 Arc<Box<T>> 的形式获取当前“已提交(正式)”数据的快照(零拷贝,仅 clone Arc)
|
||
#[inline]
|
||
pub fn data_arc(&self) -> SharedBox<T> {
|
||
let guard = self.inner.read();
|
||
Arc::clone(&guard.0)
|
||
}
|
||
|
||
/// 获取当前(草稿若存在则返回草稿,否则返回已提交)的快照
|
||
/// 这也是零拷贝:只 clone Arc,不 clone T
|
||
#[inline]
|
||
pub fn latest_arc(&self) -> SharedBox<T> {
|
||
let guard = self.inner.read();
|
||
guard.1.clone().unwrap_or_else(|| Arc::clone(&guard.0))
|
||
}
|
||
|
||
/// 通过闭包以可变方式编辑草稿(在闭包中我们给出 &mut T)
|
||
/// - 延迟拷贝:如果只有这一个 Arc 引用,则直接修改,不会克隆 T;
|
||
/// - 若草稿被其他读者共享,Arc::make_mut 会做一次 T.clone(最小必要拷贝)。
|
||
#[inline]
|
||
pub fn edit_draft<F, R>(&self, f: F) -> R
|
||
where
|
||
F: FnOnce(&mut T) -> R,
|
||
{
|
||
// 先获得写锁以创建或取出草稿 Arc 的可变引用位置
|
||
let mut guard = self.inner.write();
|
||
let mut draft_arc = if guard.1.is_none() {
|
||
Arc::clone(&guard.0)
|
||
} else {
|
||
#[allow(clippy::unwrap_used)]
|
||
guard.1.take().unwrap()
|
||
};
|
||
drop(guard);
|
||
// Arc::make_mut: 如果只有一个引用则返回可变引用;否则会克隆底层 Box<T>(要求 T: Clone)
|
||
let boxed = Arc::make_mut(&mut draft_arc); // &mut Box<T>
|
||
// 对 Box<T> 解引用得到 &mut T
|
||
let result = f(&mut **boxed);
|
||
// 恢复修改后的草稿 Arc
|
||
self.inner.write().1 = Some(draft_arc);
|
||
result
|
||
}
|
||
|
||
/// 将草稿提交到已提交位置(替换),并清除草稿
|
||
#[inline]
|
||
pub fn apply(&self) {
|
||
let mut guard = self.inner.write();
|
||
if let Some(d) = guard.1.take() {
|
||
guard.0 = d;
|
||
}
|
||
}
|
||
|
||
/// 丢弃草稿(如果存在)
|
||
#[inline]
|
||
pub fn discard(&self) {
|
||
let mut guard = self.inner.write();
|
||
guard.1 = None;
|
||
}
|
||
|
||
/// 异步地以拥有 Box<T> 的方式修改已提交数据:将克隆一次已提交数据到本地,
|
||
/// 异步闭包返回新的 Box<T>(替换已提交数据)和业务返回值 R。
|
||
#[inline]
|
||
pub async fn with_data_modify<F, Fut, R>(&self, f: F) -> Result<R, anyhow::Error>
|
||
where
|
||
T: Send + Sync + 'static,
|
||
F: FnOnce(Box<T>) -> Fut + Send,
|
||
Fut: std::future::Future<Output = Result<(Box<T>, R), anyhow::Error>> + Send,
|
||
{
|
||
// 读取已提交快照(cheap Arc clone, 然后得到 Box<T> 所有权 via clone)
|
||
// 注意:为了让闭包接收 Box<T> 所有权,我们需要 clone 底层 T(不可避免)
|
||
let local: Box<T> = {
|
||
let guard = self.inner.read();
|
||
// 将 Arc<Box<T>> 的 Box<T> clone 出来(会调用 T: Clone)
|
||
(*guard.0).clone()
|
||
};
|
||
|
||
let (new_local, res) = f(local).await?;
|
||
|
||
// 将新的 Box<T> 放到已提交位置(包进 Arc)
|
||
self.inner.write().0 = Arc::new(new_local);
|
||
|
||
Ok(res)
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use anyhow::anyhow;
|
||
use std::future::Future;
|
||
use std::pin::Pin;
|
||
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
|
||
|
||
#[derive(Clone, Debug, Default, PartialEq)]
|
||
struct IVerge {
|
||
enable_auto_launch: Option<bool>,
|
||
enable_tun_mode: Option<bool>,
|
||
}
|
||
|
||
// Minimal single-threaded executor for immediately-ready futures
|
||
fn block_on_ready<F: Future>(fut: F) -> F::Output {
|
||
fn no_op_raw_waker() -> RawWaker {
|
||
fn clone(_: *const ()) -> RawWaker {
|
||
no_op_raw_waker()
|
||
}
|
||
fn wake(_: *const ()) {}
|
||
fn wake_by_ref(_: *const ()) {}
|
||
fn drop(_: *const ()) {}
|
||
static VTABLE: RawWakerVTable = RawWakerVTable::new(clone, wake, wake_by_ref, drop);
|
||
RawWaker::new(std::ptr::null(), &VTABLE)
|
||
}
|
||
|
||
let waker = unsafe { Waker::from_raw(no_op_raw_waker()) };
|
||
let mut cx = Context::from_waker(&waker);
|
||
let mut fut = Box::pin(fut);
|
||
loop {
|
||
match Pin::as_mut(&mut fut).poll(&mut cx) {
|
||
Poll::Ready(v) => return v,
|
||
Poll::Pending => std::thread::yield_now(),
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_draft_basic_flow() {
|
||
let verge = IVerge {
|
||
enable_auto_launch: Some(true),
|
||
enable_tun_mode: Some(false),
|
||
};
|
||
let draft = Draft::new(verge);
|
||
|
||
// 读取正式数据(data_arc)
|
||
{
|
||
let data = draft.data_arc();
|
||
assert_eq!(data.enable_auto_launch, Some(true));
|
||
assert_eq!(data.enable_tun_mode, Some(false));
|
||
}
|
||
|
||
// 修改草稿(使用 edit_draft)
|
||
draft.edit_draft(|d| {
|
||
d.enable_auto_launch = Some(false);
|
||
d.enable_tun_mode = Some(true);
|
||
});
|
||
|
||
// 正式数据未变
|
||
{
|
||
let data = draft.data_arc();
|
||
assert_eq!(data.enable_auto_launch, Some(true));
|
||
assert_eq!(data.enable_tun_mode, Some(false));
|
||
}
|
||
|
||
// 草稿已变
|
||
{
|
||
let latest = draft.latest_arc();
|
||
assert_eq!(latest.enable_auto_launch, Some(false));
|
||
assert_eq!(latest.enable_tun_mode, Some(true));
|
||
}
|
||
|
||
// 提交草稿
|
||
draft.apply();
|
||
|
||
// 正式数据已更新
|
||
{
|
||
let data = draft.data_arc();
|
||
assert_eq!(data.enable_auto_launch, Some(false));
|
||
assert_eq!(data.enable_tun_mode, Some(true));
|
||
}
|
||
|
||
// 新一轮草稿并修改
|
||
draft.edit_draft(|d| {
|
||
d.enable_auto_launch = Some(true);
|
||
});
|
||
{
|
||
let latest = draft.latest_arc();
|
||
assert_eq!(latest.enable_auto_launch, Some(true));
|
||
assert_eq!(latest.enable_tun_mode, Some(true));
|
||
}
|
||
|
||
// 丢弃草稿
|
||
draft.discard();
|
||
|
||
// 丢弃后再次创建草稿,会从已提交重新 clone
|
||
{
|
||
draft.edit_draft(|d| {
|
||
// 原 committed 是 enable_auto_launch = Some(false)
|
||
assert_eq!(d.enable_auto_launch, Some(false));
|
||
// 再修改一下
|
||
d.enable_tun_mode = Some(false);
|
||
});
|
||
// 草稿中值已修改,但正式数据仍是 apply 后的值
|
||
let data = draft.data_arc();
|
||
assert_eq!(data.enable_auto_launch, Some(false));
|
||
assert_eq!(data.enable_tun_mode, Some(true));
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_arc_pointer_behavior_on_edit_and_apply() {
|
||
let draft = Draft::new(IVerge {
|
||
enable_auto_launch: Some(true),
|
||
enable_tun_mode: Some(false),
|
||
});
|
||
|
||
// 初始 latest == committed
|
||
let committed = draft.data_arc();
|
||
let latest = draft.latest_arc();
|
||
assert!(std::sync::Arc::ptr_eq(&committed, &latest));
|
||
|
||
// 第一次 edit:由于与 committed 共享,Arc::make_mut 会克隆
|
||
draft.edit_draft(|d| d.enable_tun_mode = Some(true));
|
||
let committed_after_first_edit = draft.data_arc();
|
||
let draft_after_first_edit = draft.latest_arc();
|
||
assert!(!std::sync::Arc::ptr_eq(
|
||
&committed_after_first_edit,
|
||
&draft_after_first_edit
|
||
));
|
||
// 提交会把 committed 指向草稿的 Arc
|
||
let prev_draft_ptr = std::sync::Arc::as_ptr(&draft_after_first_edit);
|
||
draft.apply();
|
||
let committed_after_apply = draft.data_arc();
|
||
assert_eq!(
|
||
std::sync::Arc::as_ptr(&committed_after_apply),
|
||
prev_draft_ptr
|
||
);
|
||
|
||
// 第二次编辑:此时草稿唯一持有(无其它引用),不应再克隆
|
||
// 获取草稿 Arc 的指针并立即丢弃本地引用,避免增加 strong_count
|
||
draft.edit_draft(|d| d.enable_auto_launch = Some(false));
|
||
let latest1 = draft.latest_arc();
|
||
let latest1_ptr = std::sync::Arc::as_ptr(&latest1);
|
||
drop(latest1); // 确保只有 Draft 内部持有草稿 Arc
|
||
|
||
// 再次编辑(unique,Arc::make_mut 不应克隆)
|
||
draft.edit_draft(|d| d.enable_tun_mode = Some(false));
|
||
let latest2 = draft.latest_arc();
|
||
let latest2_ptr = std::sync::Arc::as_ptr(&latest2);
|
||
|
||
assert_eq!(latest1_ptr, latest2_ptr, "Unique edit should not clone Arc");
|
||
assert_eq!(latest2.enable_auto_launch, Some(false));
|
||
assert_eq!(latest2.enable_tun_mode, Some(false));
|
||
}
|
||
|
||
#[test]
|
||
fn test_discard_restores_latest_to_committed() {
|
||
let draft = Draft::new(IVerge {
|
||
enable_auto_launch: Some(false),
|
||
enable_tun_mode: Some(false),
|
||
});
|
||
|
||
// 创建草稿并修改
|
||
draft.edit_draft(|d| d.enable_auto_launch = Some(true));
|
||
let committed = draft.data_arc();
|
||
let latest = draft.latest_arc();
|
||
assert!(!std::sync::Arc::ptr_eq(&committed, &latest));
|
||
|
||
// 丢弃草稿后 latest 应回到 committed
|
||
draft.discard();
|
||
let committed2 = draft.data_arc();
|
||
let latest2 = draft.latest_arc();
|
||
assert!(std::sync::Arc::ptr_eq(&committed2, &latest2));
|
||
assert_eq!(latest2.enable_auto_launch, Some(false));
|
||
}
|
||
|
||
#[test]
|
||
fn test_edit_draft_returns_closure_result() {
|
||
let draft = Draft::new(IVerge::default());
|
||
let ret = draft.edit_draft(|d| {
|
||
d.enable_tun_mode = Some(true);
|
||
123usize
|
||
});
|
||
assert_eq!(ret, 123);
|
||
let latest = draft.latest_arc();
|
||
assert_eq!(latest.enable_tun_mode, Some(true));
|
||
}
|
||
|
||
#[test]
|
||
fn test_with_data_modify_ok_and_replaces_committed() {
|
||
let draft = Draft::new(IVerge {
|
||
enable_auto_launch: Some(false),
|
||
enable_tun_mode: Some(false),
|
||
});
|
||
|
||
// 使用 with_data_modify 异步(立即就绪)地更新 committed
|
||
let res = block_on_ready(draft.with_data_modify(|mut v| async move {
|
||
v.enable_auto_launch = Some(true);
|
||
Ok((Box::new(*v), "done")) // Dereference v to get Box<T>
|
||
}));
|
||
assert_eq!(
|
||
{
|
||
#[allow(clippy::unwrap_used)]
|
||
res.unwrap()
|
||
},
|
||
"done"
|
||
);
|
||
|
||
let committed = draft.data_arc();
|
||
assert_eq!(committed.enable_auto_launch, Some(true));
|
||
assert_eq!(committed.enable_tun_mode, Some(false));
|
||
}
|
||
|
||
#[test]
|
||
fn test_with_data_modify_error_propagation() {
|
||
let draft = Draft::new(IVerge::default());
|
||
|
||
#[allow(clippy::unwrap_used)]
|
||
let err = block_on_ready(draft.with_data_modify(|v| async move {
|
||
drop(v);
|
||
Err::<(Box<IVerge>, ()), _>(anyhow!("boom"))
|
||
}))
|
||
.unwrap_err();
|
||
|
||
assert_eq!(format!("{err}"), "boom");
|
||
}
|
||
|
||
#[test]
|
||
fn test_with_data_modify_does_not_touch_existing_draft() {
|
||
let draft = Draft::new(IVerge {
|
||
enable_auto_launch: Some(false),
|
||
enable_tun_mode: Some(false),
|
||
});
|
||
|
||
// 创建草稿并修改
|
||
draft.edit_draft(|d| {
|
||
d.enable_auto_launch = Some(true);
|
||
d.enable_tun_mode = Some(true);
|
||
});
|
||
let draft_before = draft.latest_arc();
|
||
let draft_before_ptr = std::sync::Arc::as_ptr(&draft_before);
|
||
|
||
// 同时通过 with_data_modify 修改 committed
|
||
#[allow(clippy::unwrap_used)]
|
||
block_on_ready(draft.with_data_modify(|mut v| async move {
|
||
v.enable_auto_launch = Some(false); // 与草稿不同
|
||
Ok((Box::new(*v), ())) // Dereference v to get Box<T>
|
||
}))
|
||
.unwrap();
|
||
|
||
// 草稿应保持不变
|
||
let draft_after = draft.latest_arc();
|
||
assert_eq!(
|
||
std::sync::Arc::as_ptr(&draft_after),
|
||
draft_before_ptr,
|
||
"Existing draft should not be replaced by with_data_modify"
|
||
);
|
||
assert_eq!(draft_after.enable_auto_launch, Some(true));
|
||
assert_eq!(draft_after.enable_tun_mode, Some(true));
|
||
|
||
// 丢弃草稿后 latest == committed,且 committed 为异步修改结果
|
||
draft.discard();
|
||
let latest = draft.latest_arc();
|
||
assert_eq!(latest.enable_auto_launch, Some(false));
|
||
assert_eq!(latest.enable_tun_mode, Some(false));
|
||
}
|
||
}
|