0508 1.2s轮询同步
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 273 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 335 KiB |
Generated
+78
@@ -670,6 +670,15 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fsevent-sys"
|
||||||
|
version = "4.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures"
|
name = "futures"
|
||||||
version = "0.3.32"
|
version = "0.3.32"
|
||||||
@@ -1140,6 +1149,26 @@ dependencies = [
|
|||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "inotify"
|
||||||
|
version = "0.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bd5b3eaf1a28b758ac0faa5a4254e8ab2705605496f1b1f3fbbc3988ad73d199"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"inotify-sys",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "inotify-sys"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "interpolator"
|
name = "interpolator"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@@ -1225,6 +1254,26 @@ version = "0.2.19"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37"
|
checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "kqueue"
|
||||||
|
version = "1.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a"
|
||||||
|
dependencies = [
|
||||||
|
"kqueue-sys",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "kqueue-sys"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a7b65860415f949f23fa882e669f2dbd4a0f0eeb1acdd56790b30494afd7da2f"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "lazy_static"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
@@ -1438,6 +1487,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
|
checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
|
"log",
|
||||||
"wasi",
|
"wasi",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
@@ -1476,6 +1526,7 @@ dependencies = [
|
|||||||
"futures-util",
|
"futures-util",
|
||||||
"leptos",
|
"leptos",
|
||||||
"mnote-editor-core",
|
"mnote-editor-core",
|
||||||
|
"notify",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -1494,6 +1545,33 @@ version = "0.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "60993920e071b0c9b66f14e2b32740a4e27ffc82854dcd72035887f336a09a28"
|
checksum = "60993920e071b0c9b66f14e2b32740a4e27ffc82854dcd72035887f336a09a28"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "notify"
|
||||||
|
version = "8.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"fsevent-sys",
|
||||||
|
"inotify",
|
||||||
|
"kqueue",
|
||||||
|
"libc",
|
||||||
|
"log",
|
||||||
|
"mio",
|
||||||
|
"notify-types",
|
||||||
|
"walkdir",
|
||||||
|
"windows-sys 0.60.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "notify-types"
|
||||||
|
version = "2.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nu-ansi-term"
|
name = "nu-ansi-term"
|
||||||
version = "0.50.3"
|
version = "0.50.3"
|
||||||
|
|||||||
@@ -23,4 +23,5 @@ tracing-subscriber = { version = "0.3", features = ["fmt"] }
|
|||||||
tower = "0.5"
|
tower = "0.5"
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
comrak = { version = "0.52", default-features = false }
|
comrak = { version = "0.52", default-features = false }
|
||||||
|
notify = "8.2.0"
|
||||||
time = { version = "0.3", features = ["formatting"] }
|
time = { version = "0.3", features = ["formatting"] }
|
||||||
|
|||||||
@@ -0,0 +1,256 @@
|
|||||||
|
use crate::context::RequestContext;
|
||||||
|
use crate::error::WebError;
|
||||||
|
use crate::routes::local_folder_source::decode_local_id_segment;
|
||||||
|
use axum::extract::{Extension, Query};
|
||||||
|
use axum::http::{HeaderMap, HeaderName, HeaderValue};
|
||||||
|
use axum::response::sse::{Event as SseEvent, KeepAlive, Sse};
|
||||||
|
use futures_util::stream;
|
||||||
|
use notify::event::ModifyKind;
|
||||||
|
use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde_json::{json, Value};
|
||||||
|
use std::convert::Infallible;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct LocalFolderEventsQuery {
|
||||||
|
pub root_uri: String,
|
||||||
|
pub document_id: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn local_folder_events(
|
||||||
|
Extension(context): Extension<RequestContext>,
|
||||||
|
Query(query): Query<LocalFolderEventsQuery>,
|
||||||
|
) -> Result<
|
||||||
|
(
|
||||||
|
HeaderMap,
|
||||||
|
Sse<impl futures_util::Stream<Item = Result<SseEvent, Infallible>>>,
|
||||||
|
),
|
||||||
|
WebError,
|
||||||
|
> {
|
||||||
|
let root = parse_file_root_uri(&query.root_uri)?;
|
||||||
|
let canonical_root = root.canonicalize().map_err(|error| {
|
||||||
|
WebError::bad_request_code(
|
||||||
|
"local_folder_unavailable",
|
||||||
|
format!("无法访问本地文件夹: {error}"),
|
||||||
|
)
|
||||||
|
.with_context(&context)
|
||||||
|
})?;
|
||||||
|
let document_relative_path = query
|
||||||
|
.document_id
|
||||||
|
.as_deref()
|
||||||
|
.and_then(local_markdown_relative_path_from_document_id);
|
||||||
|
|
||||||
|
let (sender, receiver) = mpsc::unbounded_channel::<Value>();
|
||||||
|
let (watch_sender, mut watch_receiver) =
|
||||||
|
mpsc::unbounded_channel::<notify::Result<Event>>();
|
||||||
|
let mut watcher = RecommendedWatcher::new(
|
||||||
|
move |result| {
|
||||||
|
let _ = watch_sender.send(result);
|
||||||
|
},
|
||||||
|
Config::default(),
|
||||||
|
)
|
||||||
|
.map_err(|error| {
|
||||||
|
WebError::internal(format!("本地文件事件监听启动失败: {error}")).with_context(&context)
|
||||||
|
})?;
|
||||||
|
watcher
|
||||||
|
.watch(&canonical_root, RecursiveMode::Recursive)
|
||||||
|
.map_err(|error| {
|
||||||
|
WebError::internal(format!("本地文件夹监听失败: {error}")).with_context(&context)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let root_for_task = canonical_root.clone();
|
||||||
|
let root_uri = query.root_uri.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let _watcher = watcher;
|
||||||
|
while let Some(result) = watch_receiver.recv().await {
|
||||||
|
let Ok(event) = result else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if !should_emit_event_kind(&event.kind) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for path in event.paths {
|
||||||
|
if !is_markdown_path(&path) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(relative_path) = relative_path_string(&root_for_task, &path) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if let Some(expected) = document_relative_path.as_deref() {
|
||||||
|
if expected != relative_path {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let payload = json!({
|
||||||
|
"sourceKind": "local_folder",
|
||||||
|
"rootUri": root_uri,
|
||||||
|
"relativePath": relative_path,
|
||||||
|
"documentId": format!("local-md:{}", encode_local_id_segment(&relative_path)),
|
||||||
|
"eventKind": format!("{:?}", event.kind),
|
||||||
|
"revision": event_revision(&path),
|
||||||
|
});
|
||||||
|
if sender.send(payload).is_err() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let initial = json!({
|
||||||
|
"sourceKind": "local_folder",
|
||||||
|
"rootUri": query.root_uri,
|
||||||
|
"documentId": query.document_id,
|
||||||
|
"revision": system_time_ms(SystemTime::now()),
|
||||||
|
});
|
||||||
|
let stream = stream::unfold((Some(initial), receiver), |(initial, mut receiver)| async move {
|
||||||
|
if let Some(payload) = initial {
|
||||||
|
return Some((Ok(stream_event("ready", &payload)), (None, receiver)));
|
||||||
|
}
|
||||||
|
receiver
|
||||||
|
.recv()
|
||||||
|
.await
|
||||||
|
.map(|payload| (Ok(stream_event("change", &payload)), (None, receiver)))
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
if let Ok(name) = HeaderName::from_lowercase(b"x-mnote-web-owner") {
|
||||||
|
headers.insert(name, HeaderValue::from_static("mnote-web"));
|
||||||
|
}
|
||||||
|
if let Ok(name) = HeaderName::from_lowercase(b"x-mnote-local-folder-events-owner") {
|
||||||
|
headers.insert(name, HeaderValue::from_static("rust-web"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
headers,
|
||||||
|
Sse::new(stream).keep_alive(
|
||||||
|
KeepAlive::new()
|
||||||
|
.interval(Duration::from_secs(15))
|
||||||
|
.text("keepalive"),
|
||||||
|
),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_file_root_uri(root_uri: &str) -> Result<PathBuf, WebError> {
|
||||||
|
let trimmed = root_uri.trim();
|
||||||
|
let Some(path) = trimmed.strip_prefix("file://") else {
|
||||||
|
return Err(WebError::bad_request_code(
|
||||||
|
"local_folder_root_invalid",
|
||||||
|
"本地文件夹 rootUri 必须是 file:// URI",
|
||||||
|
));
|
||||||
|
};
|
||||||
|
Ok(PathBuf::from(path))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn local_markdown_relative_path_from_document_id(document_id: &str) -> Option<String> {
|
||||||
|
let trimmed = document_id.trim();
|
||||||
|
let encoded = trimmed.strip_prefix("local-md:")?;
|
||||||
|
decode_local_id_segment(encoded).ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn relative_path_string(root: &Path, path: &Path) -> Option<String> {
|
||||||
|
path.strip_prefix(root)
|
||||||
|
.ok()
|
||||||
|
.map(|relative| relative.to_string_lossy().replace('\\', "/"))
|
||||||
|
.filter(|relative| !relative.is_empty())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_markdown_path(path: &Path) -> bool {
|
||||||
|
path.extension()
|
||||||
|
.and_then(|extension| extension.to_str())
|
||||||
|
.map(|extension| {
|
||||||
|
extension.eq_ignore_ascii_case("md") || extension.eq_ignore_ascii_case("markdown")
|
||||||
|
})
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_emit_event_kind(kind: &EventKind) -> bool {
|
||||||
|
match kind {
|
||||||
|
EventKind::Create(_) | EventKind::Remove(_) => true,
|
||||||
|
EventKind::Modify(modify_kind) => matches!(
|
||||||
|
modify_kind,
|
||||||
|
ModifyKind::Any | ModifyKind::Data(_) | ModifyKind::Name(_) | ModifyKind::Metadata(_)
|
||||||
|
),
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_local_id_segment(value: &str) -> String {
|
||||||
|
let mut encoded = String::with_capacity(value.len());
|
||||||
|
for byte in value.as_bytes() {
|
||||||
|
let character = *byte as char;
|
||||||
|
if character.is_ascii_alphanumeric() || matches!(character, '.' | '_' | '-') {
|
||||||
|
encoded.push(character);
|
||||||
|
} else {
|
||||||
|
encoded.push('~');
|
||||||
|
encoded.push_str(&format!("{byte:02X}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
encoded
|
||||||
|
}
|
||||||
|
|
||||||
|
fn event_revision(path: &Path) -> u128 {
|
||||||
|
std::fs::metadata(path)
|
||||||
|
.ok()
|
||||||
|
.and_then(|metadata| metadata.modified().ok())
|
||||||
|
.map(system_time_ms)
|
||||||
|
.unwrap_or_else(|| system_time_ms(SystemTime::now()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn system_time_ms(time: SystemTime) -> u128 {
|
||||||
|
time.duration_since(UNIX_EPOCH)
|
||||||
|
.map(|duration| duration.as_millis())
|
||||||
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stream_event(event_name: &str, payload: &Value) -> SseEvent {
|
||||||
|
let id = payload
|
||||||
|
.get("revision")
|
||||||
|
.and_then(|value| {
|
||||||
|
value
|
||||||
|
.as_str()
|
||||||
|
.map(ToOwned::to_owned)
|
||||||
|
.or_else(|| value.as_u64().map(|number| number.to_string()))
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| "0".to_string());
|
||||||
|
SseEvent::default()
|
||||||
|
.event(event_name)
|
||||||
|
.id(id)
|
||||||
|
.json_data(payload)
|
||||||
|
.expect("SSE 事件必须可序列化")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn local_markdown_document_id_maps_to_relative_path() {
|
||||||
|
assert_eq!(
|
||||||
|
local_markdown_relative_path_from_document_id("local-md:docs~2FREADME.md")
|
||||||
|
.as_deref(),
|
||||||
|
Some("docs/README.md")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn markdown_path_filter_accepts_markdown_files_only() {
|
||||||
|
assert!(is_markdown_path(Path::new("README.md")));
|
||||||
|
assert!(is_markdown_path(Path::new("README.markdown")));
|
||||||
|
assert!(!is_markdown_path(Path::new("image.png")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn event_kind_filter_ignores_access_events() {
|
||||||
|
assert!(should_emit_event_kind(&EventKind::Create(notify::event::CreateKind::File)));
|
||||||
|
assert!(should_emit_event_kind(&EventKind::Modify(ModifyKind::Data(
|
||||||
|
notify::event::DataChange::Content,
|
||||||
|
))));
|
||||||
|
assert!(!should_emit_event_kind(&EventKind::Access(
|
||||||
|
notify::event::AccessKind::Read,
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2088,7 +2088,7 @@ fn encode_local_id_segment(value: &str) -> String {
|
|||||||
encoded
|
encoded
|
||||||
}
|
}
|
||||||
|
|
||||||
fn decode_local_id_segment(value: &str) -> Result<String, WebError> {
|
pub(crate) fn decode_local_id_segment(value: &str) -> Result<String, WebError> {
|
||||||
let bytes = value.as_bytes();
|
let bytes = value.as_bytes();
|
||||||
let mut decoded = Vec::with_capacity(bytes.len());
|
let mut decoded = Vec::with_capacity(bytes.len());
|
||||||
let mut index = 0;
|
let mut index = 0;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ mod health;
|
|||||||
mod hermes;
|
mod hermes;
|
||||||
mod kernel;
|
mod kernel;
|
||||||
mod local_folder_source;
|
mod local_folder_source;
|
||||||
|
mod local_folder_events;
|
||||||
mod local_markdown_parser;
|
mod local_markdown_parser;
|
||||||
mod mindmap_shell;
|
mod mindmap_shell;
|
||||||
mod query_support;
|
mod query_support;
|
||||||
@@ -97,6 +98,10 @@ pub fn build_router(state: AppState) -> Router {
|
|||||||
"/api/tree/local-folder-watch",
|
"/api/tree/local-folder-watch",
|
||||||
get(tree::local_folder_watch),
|
get(tree::local_folder_watch),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/api/local-folder/events",
|
||||||
|
get(local_folder_events::local_folder_events),
|
||||||
|
)
|
||||||
.route(
|
.route(
|
||||||
"/api/tree/runtime/reduce",
|
"/api/tree/runtime/reduce",
|
||||||
post(tree::reduce_tree_shell_runtime),
|
post(tree::reduce_tree_shell_runtime),
|
||||||
|
|||||||
@@ -693,8 +693,7 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
|
|||||||
let lastSavedSerialized = '';
|
let lastSavedSerialized = '';
|
||||||
let hasPendingLocalChanges = false;
|
let hasPendingLocalChanges = false;
|
||||||
let suppressNextHostSyncChange = false;
|
let suppressNextHostSyncChange = false;
|
||||||
let localExternalPollTimer = 0;
|
let localExternalEvents = null;
|
||||||
let localExternalPollInFlight = false;
|
|
||||||
let lastExternalConflictDetectionKey = editorMeta.conflictDetectionKey || '';
|
let lastExternalConflictDetectionKey = editorMeta.conflictDetectionKey || '';
|
||||||
const normalizeBridgeValue = (value) => {
|
const normalizeBridgeValue = (value) => {
|
||||||
if (value instanceof Map) {
|
if (value instanceof Map) {
|
||||||
@@ -858,10 +857,8 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
|
|||||||
setStatus('synced-external-change');
|
setStatus('synced-external-change');
|
||||||
};
|
};
|
||||||
|
|
||||||
const pollLocalMarkdownExternalChange = async () => {
|
const refreshFromExternalFileChange = async () => {
|
||||||
if (bootstrap.sourceKind !== 'local_folder' || !bootstrap.rootUri || document.hidden) return;
|
if (bootstrap.sourceKind !== 'local_folder' || !bootstrap.rootUri || document.hidden) return;
|
||||||
if (localExternalPollInFlight) return;
|
|
||||||
localExternalPollInFlight = true;
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(pageAggregateUrl().toString(), {
|
const response = await fetch(pageAggregateUrl().toString(), {
|
||||||
cache: 'no-store',
|
cache: 'no-store',
|
||||||
@@ -883,11 +880,25 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
|
|||||||
dispatchReplaceContent(nextAggregate);
|
dispatchReplaceContent(nextAggregate);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('mnote local folder 外部更新检测失败', error);
|
console.warn('mnote local folder 外部更新检测失败', error);
|
||||||
} finally {
|
|
||||||
localExternalPollInFlight = false;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const connectLocalMarkdownExternalEvents = () => {
|
||||||
|
if (bootstrap.sourceKind !== 'local_folder' || !bootstrap.rootUri || typeof window.EventSource !== 'function') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const url = new URL('/api/local-folder/events', window.location.origin);
|
||||||
|
url.searchParams.set('rootUri', bootstrap.rootUri);
|
||||||
|
url.searchParams.set('documentId', bootstrap.documentId);
|
||||||
|
localExternalEvents = new EventSource(url.toString());
|
||||||
|
localExternalEvents.addEventListener('change', () => {
|
||||||
|
void refreshFromExternalFileChange();
|
||||||
|
});
|
||||||
|
localExternalEvents.onerror = () => {
|
||||||
|
console.warn('mnote local folder 外部更新事件流中断,将等待浏览器自动重连');
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const start = async () => {
|
const start = async () => {
|
||||||
setStatus('loading-assets');
|
setStatus('loading-assets');
|
||||||
const manifestResponse = await fetch('/api/leptos-tiptap-runtime/manifest.json', { cache: 'no-store' });
|
const manifestResponse = await fetch('/api/leptos-tiptap-runtime/manifest.json', { cache: 'no-store' });
|
||||||
@@ -909,12 +920,7 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
|
|||||||
if (typeof window.__mnoteApplyPageOptionsToShell === 'function') {
|
if (typeof window.__mnoteApplyPageOptionsToShell === 'function') {
|
||||||
window.__mnoteApplyPageOptionsToShell();
|
window.__mnoteApplyPageOptionsToShell();
|
||||||
}
|
}
|
||||||
if (bootstrap.sourceKind === 'local_folder' && bootstrap.rootUri) {
|
connectLocalMarkdownExternalEvents();
|
||||||
void pollLocalMarkdownExternalChange();
|
|
||||||
localExternalPollTimer = window.setInterval(() => {
|
|
||||||
void pollLocalMarkdownExternalChange();
|
|
||||||
}, 1200);
|
|
||||||
}
|
|
||||||
setStatus('ready');
|
setStatus('ready');
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1640,10 +1646,12 @@ mod tests {
|
|||||||
assert!(html.contains("asset.png"));
|
assert!(html.contains("asset.png"));
|
||||||
assert!(html.contains("data-row-kind=\"markdown\""));
|
assert!(html.contains("data-row-kind=\"markdown\""));
|
||||||
assert!(html.contains("data-mnote-action=\"open-local-folder\""));
|
assert!(html.contains("data-mnote-action=\"open-local-folder\""));
|
||||||
assert!(html.contains("pollLocalMarkdownExternalChange"));
|
assert!(html.contains("refreshFromExternalFileChange"));
|
||||||
assert!(html.contains("/api/page-aggregate/${encodeURIComponent(bootstrap.documentId)}"));
|
assert!(html.contains("/api/local-folder/events"));
|
||||||
|
assert!(html.contains("new EventSource(url.toString())"));
|
||||||
assert!(html.contains("command: 'replaceContent'"));
|
assert!(html.contains("command: 'replaceContent'"));
|
||||||
assert!(html.contains("external-change-conflict"));
|
assert!(html.contains("external-change-conflict"));
|
||||||
|
assert!(!html.contains("setInterval(() => {\n void pollLocalMarkdownExternalChange();\n }, 1200);"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
Reference in New Issue
Block a user