feat: consolidate local-first mnote web runtime
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user