feat: consolidate local-first mnote web runtime

This commit is contained in:
lix-2026
2026-05-28 22:01:44 +08:00
parent 7354807ee9
commit 39b9a0183a
154 changed files with 13591 additions and 12728 deletions
@@ -71,6 +71,12 @@ struct BufferStoreInner {
buffers: HashMap<BufferKey, DocumentBuffer>,
}
#[derive(Debug, Clone)]
pub struct BufferExternalModificationOutcome {
pub buffer: DocumentBuffer,
pub self_write_echo: bool,
}
impl BufferStore {
pub fn new() -> Self {
Self {
@@ -102,6 +108,8 @@ impl BufferStore {
last_loaded_at: Some(now_ms),
last_saved_at: Some(now_ms),
external_actor: None,
last_write_intent_id: None,
last_save_operation_id: None,
};
inner.buffers.insert(key, buf.clone());
buf
@@ -131,11 +139,27 @@ impl BufferStore {
path: &ObjectWorkspacePath,
file_version: String,
content_hash: String,
) -> Option<DocumentBuffer> {
self.mark_saved_with_operation(path, file_version, content_hash, None, None)
}
pub fn mark_saved_with_operation(
&self,
path: &ObjectWorkspacePath,
file_version: String,
content_hash: String,
write_intent_id: Option<String>,
save_operation_id: Option<String>,
) -> Option<DocumentBuffer> {
let key = BufferKey::from_workspace_path(path);
let mut inner = self.inner.write().expect("BufferStore lock");
if let Some(buf) = inner.buffers.get_mut(&key) {
buf.mark_saved(file_version, content_hash);
buf.mark_saved_with_operation(
file_version,
content_hash,
write_intent_id,
save_operation_id,
);
Some(buf.clone())
} else {
None
@@ -158,6 +182,54 @@ impl BufferStore {
}
}
/// 带磁盘版本的外部修改标记。
///
/// watcher 会看到 MNote 自己保存产生的文件系统回声;如果 watcher 观测到的
/// `observed_file_version` 等于最近保存/加载的 `file_version`,说明这不是新的外部修改,
/// 不应把 Clean 变成 ExternalModified,也不应把当前浏览器正在输入的 Dirty 变成 Stale。
pub fn mark_external_modified_if_version_changed(
&self,
path: &ObjectWorkspacePath,
observed_file_version: Option<String>,
actor: Option<String>,
) -> Option<DocumentBuffer> {
self.mark_external_modified_if_version_changed_with_outcome(
path,
observed_file_version,
actor,
)
.map(|outcome| outcome.buffer)
}
pub fn mark_external_modified_if_version_changed_with_outcome(
&self,
path: &ObjectWorkspacePath,
observed_file_version: Option<String>,
actor: Option<String>,
) -> Option<BufferExternalModificationOutcome> {
let key = BufferKey::from_workspace_path(path);
let mut inner = self.inner.write().expect("BufferStore lock");
let buf = inner.buffers.get_mut(&key)?;
if let Some(observed) = observed_file_version
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
{
if buf.file_version.as_deref() == Some(observed) {
return Some(BufferExternalModificationOutcome {
buffer: buf.clone(),
self_write_echo: true,
});
}
buf.file_version = Some(observed.to_string());
}
buf.mark_external_modified(actor);
Some(BufferExternalModificationOutcome {
buffer: buf.clone(),
self_write_echo: false,
})
}
/// 标记 buffer 为 dirty(编辑器内容已修改)。
pub fn mark_dirty(
&self,
@@ -348,6 +420,8 @@ mod tests {
last_loaded_at: Some(1_700_000_000_000i64),
last_saved_at: Some(1_700_000_000_001i64),
external_actor: None,
last_write_intent_id: None,
last_save_operation_id: None,
}
}
@@ -411,6 +485,99 @@ mod tests {
);
}
#[test]
fn document_buffer_ignores_watcher_echo_for_last_saved_version() {
let store = BufferStore::new();
let path = make_test_workspace_path("ws_1", "doc.md", "local-md:doc.md");
let key = BufferKey::from_workspace_path(&path);
store.update(&key, make_fresh_buffer(&path));
let clean_echo = store
.mark_external_modified_if_version_changed(
&path,
Some("v1".into()),
Some("external-editor".into()),
)
.expect("buffer");
assert_eq!(clean_echo.dirty_state, DocBufferDirtyState::Clean);
assert_eq!(clean_echo.external_actor, None);
let dirty = store
.mark_dirty(&path, "sha256:dirty".into())
.expect("dirty");
assert_eq!(dirty.dirty_state, DocBufferDirtyState::Dirty);
let dirty_echo = store
.mark_external_modified_if_version_changed(
&path,
Some("v1".into()),
Some("external-editor".into()),
)
.expect("buffer");
assert_eq!(dirty_echo.dirty_state, DocBufferDirtyState::Dirty);
assert_eq!(dirty_echo.external_actor, None);
}
#[test]
fn document_buffer_marks_stale_when_dirty_buffer_sees_new_version() {
let store = BufferStore::new();
let path = make_test_workspace_path("ws_1", "doc.md", "local-md:doc.md");
let key = BufferKey::from_workspace_path(&path);
let mut buf = make_fresh_buffer(&path);
buf.mark_dirty("sha256:dirty".into());
store.update(&key, buf);
let result = store
.mark_external_modified_if_version_changed(
&path,
Some("v2".into()),
Some("external-editor".into()),
)
.expect("buffer");
assert_eq!(result.dirty_state, DocBufferDirtyState::Stale);
assert_eq!(result.file_version.as_deref(), Some("v2"));
assert_eq!(result.external_actor.as_deref(), Some("external-editor"));
}
#[test]
fn document_buffer_records_save_operation_and_reports_self_write_echo() {
let store = BufferStore::new();
let path = make_test_workspace_path("ws_1", "doc.md", "local-md:doc.md");
let key = BufferKey::from_workspace_path(&path);
let mut buf = make_fresh_buffer(&path);
buf.mark_dirty("sha256:dirty".into());
store.update(&key, buf);
let saved = store
.mark_saved_with_operation(
&path,
"v2".into(),
"sha256:v2".into(),
Some("intent:editor:abc".into()),
Some("save:op:abc".into()),
)
.expect("保存后应更新 buffer");
assert_eq!(saved.last_write_intent_id.as_deref(), Some("intent:editor:abc"));
assert_eq!(saved.last_save_operation_id.as_deref(), Some("save:op:abc"));
let outcome = store
.mark_external_modified_if_version_changed_with_outcome(
&path,
Some("v2".into()),
Some("external-editor".into()),
)
.expect("watcher 回声应返回 outcome");
assert!(outcome.self_write_echo, "同版本 watcher 回声应标记为自写回声");
assert_eq!(
outcome.buffer.last_write_intent_id.as_deref(),
Some("intent:editor:abc")
);
assert_eq!(
outcome.buffer.last_save_operation_id.as_deref(),
Some("save:op:abc")
);
assert_eq!(outcome.buffer.dirty_state, DocBufferDirtyState::Clean);
}
#[test]
fn document_buffer_mark_saved_after_page_body_write() {
let store = BufferStore::new();