Improve local filetree view state and sidebar performance
This commit is contained in:
@@ -10,6 +10,7 @@ use crate::routes::documents::{
|
||||
use crate::routes::gateway::default_workspace_name_for_context;
|
||||
use crate::routes::local_folder_source::{
|
||||
ensure_local_workspace_read_access_with_state, is_local_access_policy_admin_context,
|
||||
load_local_folder_file_tree_children_snapshot,
|
||||
load_local_folder_file_tree_snapshot_with_reveal, load_local_folder_page_tree_snapshot,
|
||||
resolve_local_markdown_page_aggregate,
|
||||
};
|
||||
@@ -35,7 +36,7 @@ use axum::Json;
|
||||
use bridge_runtime::RuntimeQueryEnvelopeWire;
|
||||
use core_protocol::KernelProjectionKind;
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
use serde_json::{json, Value};
|
||||
use std::fs;
|
||||
use std::path::{Component, Path as FsPath, PathBuf};
|
||||
|
||||
@@ -43,6 +44,10 @@ const HEADER_MNOTE_WEB_OWNER: &str = "x-mnote-web-owner";
|
||||
const HEADER_MNOTE_WEB_SHELL: &str = "x-mnote-web-shell";
|
||||
const COOKIE_RECENT_PAGE_ID: &str = "mnote_recent_page_id";
|
||||
|
||||
#[cfg(test)]
|
||||
static LOCAL_PAGE_AGGREGATE_TEST_BLOCK_MS: std::sync::atomic::AtomicU64 =
|
||||
std::sync::atomic::AtomicU64::new(0);
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DocumentShellQuery {
|
||||
@@ -50,6 +55,7 @@ pub struct DocumentShellQuery {
|
||||
pub source_kind: Option<String>,
|
||||
pub root_uri: Option<String>,
|
||||
pub tree_view: Option<String>,
|
||||
pub file_tree_scope: Option<String>,
|
||||
pub secondary_document_id: Option<String>,
|
||||
pub secondary_source_kind: Option<String>,
|
||||
pub secondary_root_uri: Option<String>,
|
||||
@@ -129,6 +135,7 @@ pub async fn document_page_shell(
|
||||
};
|
||||
let default_workspace_name = default_workspace_name_for_context(&state, &context);
|
||||
let mut workspace_projection = load_workspace_shell_projection(
|
||||
Some(&state),
|
||||
state.config(),
|
||||
&context,
|
||||
&workspace_id,
|
||||
@@ -148,11 +155,17 @@ pub async fn document_page_shell(
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.is_some_and(|value| value == "filetree");
|
||||
let file_tree_scope = query
|
||||
.file_tree_scope
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty());
|
||||
let (sidebar_tree_html, file_tree_html) = if is_local_folder {
|
||||
let root_uri = query.root_uri.as_deref().unwrap_or_default();
|
||||
(
|
||||
render_local_sidebar_tree_html(root_uri, Some(&document_id)).unwrap_or_default(),
|
||||
render_local_file_tree_html(root_uri, Some(&document_id), None).unwrap_or_default(),
|
||||
render_local_file_tree_html_scoped(root_uri, Some(&document_id), None, file_tree_scope)
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
@@ -179,6 +192,7 @@ pub async fn document_page_shell(
|
||||
} else {
|
||||
None
|
||||
},
|
||||
file_tree_scope,
|
||||
);
|
||||
let workspace_name = workspace_projection.workspace_name.clone();
|
||||
let page_subtree_json =
|
||||
@@ -245,6 +259,7 @@ pub async fn document_page_shell(
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>{}</title>
|
||||
{}
|
||||
<style>{}</style>
|
||||
</head>
|
||||
<body data-mnote-web-owner="mnote-web" data-mnote-shell="document" data-document-id="{}" data-mnote-source-kind="{}" data-mnote-root-uri="{}" data-secondary-requested="{}" data-secondary-invalid="{}">
|
||||
@@ -261,6 +276,7 @@ pub async fn document_page_shell(
|
||||
</body>
|
||||
</html>"#,
|
||||
escape_html(title),
|
||||
render_editor_runtime_preload_links(),
|
||||
crate::ssr::MNOTE_CSS,
|
||||
escape_html(&document_id),
|
||||
escape_html(primary_source_kind.unwrap_or("convex_workspace")),
|
||||
@@ -646,6 +662,11 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
|
||||
r#"<script type="module" src="/api/mnote-browser-runtime/document-editor-adapter-runtime.js"></script>"#
|
||||
}
|
||||
|
||||
pub(crate) fn render_editor_runtime_preload_links() -> &'static str {
|
||||
r#"<link rel="modulepreload" href="/api/leptos-tiptap-runtime/mnote-leptos-tiptap-spike-island.js">
|
||||
<link rel="preload" href="/api/leptos-tiptap-runtime/mnote-leptos-tiptap-spike-island_bg.wasm" as="fetch" type="application/wasm" crossorigin>"#
|
||||
}
|
||||
|
||||
fn runtime_asset_root() -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../spikes/leptos-tiptap-spike/generated/island")
|
||||
@@ -678,6 +699,10 @@ fn runtime_asset_content_type(asset_path: &str) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
fn runtime_asset_cache_control() -> &'static str {
|
||||
"public, max-age=3600, stale-while-revalidate=86400"
|
||||
}
|
||||
|
||||
pub async fn editor_image_placeholder_asset() -> Response {
|
||||
const SVG: &str = r##"<svg xmlns="http://www.w3.org/2000/svg" width="640" height="360" viewBox="0 0 640 360" role="img" aria-label="E24 image placeholder">
|
||||
<rect width="640" height="360" rx="18" fill="#f3f4f6"/>
|
||||
@@ -705,7 +730,7 @@ pub async fn resource_open_runtime_asset() -> Response {
|
||||
header::CONTENT_TYPE,
|
||||
"application/javascript; charset=utf-8",
|
||||
)
|
||||
.header(header::CACHE_CONTROL, "no-store")
|
||||
.header(header::CACHE_CONTROL, runtime_asset_cache_control())
|
||||
.header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
||||
.body(Body::from(JS))
|
||||
.unwrap_or_else(|_| Response::new(Body::empty()))
|
||||
@@ -719,7 +744,7 @@ pub async fn local_upload_runtime_asset() -> Response {
|
||||
header::CONTENT_TYPE,
|
||||
"application/javascript; charset=utf-8",
|
||||
)
|
||||
.header(header::CACHE_CONTROL, "no-store")
|
||||
.header(header::CACHE_CONTROL, runtime_asset_cache_control())
|
||||
.header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
||||
.body(Body::from(JS))
|
||||
.unwrap_or_else(|_| Response::new(Body::empty()))
|
||||
@@ -733,7 +758,7 @@ pub async fn sidebar_tree_runtime_asset() -> Response {
|
||||
header::CONTENT_TYPE,
|
||||
"application/javascript; charset=utf-8",
|
||||
)
|
||||
.header(header::CACHE_CONTROL, "no-store")
|
||||
.header(header::CACHE_CONTROL, runtime_asset_cache_control())
|
||||
.header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
||||
.body(Body::from(JS))
|
||||
.unwrap_or_else(|_| Response::new(Body::empty()))
|
||||
@@ -747,7 +772,7 @@ pub async fn sidebar_page_ai_runtime_asset() -> Response {
|
||||
header::CONTENT_TYPE,
|
||||
"application/javascript; charset=utf-8",
|
||||
)
|
||||
.header(header::CACHE_CONTROL, "no-store")
|
||||
.header(header::CACHE_CONTROL, runtime_asset_cache_control())
|
||||
.header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
||||
.body(Body::from(JS))
|
||||
.unwrap_or_else(|_| Response::new(Body::empty()))
|
||||
@@ -761,7 +786,7 @@ pub async fn sidebar_page_settings_runtime_asset() -> Response {
|
||||
header::CONTENT_TYPE,
|
||||
"application/javascript; charset=utf-8",
|
||||
)
|
||||
.header(header::CACHE_CONTROL, "no-store")
|
||||
.header(header::CACHE_CONTROL, runtime_asset_cache_control())
|
||||
.header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
||||
.body(Body::from(JS))
|
||||
.unwrap_or_else(|_| Response::new(Body::empty()))
|
||||
@@ -775,7 +800,7 @@ pub async fn sidebar_shell_runtime_asset() -> Response {
|
||||
header::CONTENT_TYPE,
|
||||
"application/javascript; charset=utf-8",
|
||||
)
|
||||
.header(header::CACHE_CONTROL, "no-store")
|
||||
.header(header::CACHE_CONTROL, runtime_asset_cache_control())
|
||||
.header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
||||
.body(Body::from(JS))
|
||||
.unwrap_or_else(|_| Response::new(Body::empty()))
|
||||
@@ -789,7 +814,7 @@ pub async fn sidebar_workspace_runtime_asset() -> Response {
|
||||
header::CONTENT_TYPE,
|
||||
"application/javascript; charset=utf-8",
|
||||
)
|
||||
.header(header::CACHE_CONTROL, "no-store")
|
||||
.header(header::CACHE_CONTROL, runtime_asset_cache_control())
|
||||
.header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
||||
.body(Body::from(JS))
|
||||
.unwrap_or_else(|_| Response::new(Body::empty()))
|
||||
@@ -803,7 +828,7 @@ pub async fn sidebar_page_tree_runtime_asset() -> Response {
|
||||
header::CONTENT_TYPE,
|
||||
"application/javascript; charset=utf-8",
|
||||
)
|
||||
.header(header::CACHE_CONTROL, "no-store")
|
||||
.header(header::CACHE_CONTROL, runtime_asset_cache_control())
|
||||
.header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
||||
.body(Body::from(JS))
|
||||
.unwrap_or_else(|_| Response::new(Body::empty()))
|
||||
@@ -817,7 +842,7 @@ pub async fn sidebar_tree_live_apply_runtime_asset() -> Response {
|
||||
header::CONTENT_TYPE,
|
||||
"application/javascript; charset=utf-8",
|
||||
)
|
||||
.header(header::CACHE_CONTROL, "no-store")
|
||||
.header(header::CACHE_CONTROL, runtime_asset_cache_control())
|
||||
.header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
||||
.body(Body::from(JS))
|
||||
.unwrap_or_else(|_| Response::new(Body::empty()))
|
||||
@@ -831,7 +856,7 @@ pub async fn sidebar_filetree_open_runtime_asset() -> Response {
|
||||
header::CONTENT_TYPE,
|
||||
"application/javascript; charset=utf-8",
|
||||
)
|
||||
.header(header::CACHE_CONTROL, "no-store")
|
||||
.header(header::CACHE_CONTROL, runtime_asset_cache_control())
|
||||
.header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
||||
.body(Body::from(JS))
|
||||
.unwrap_or_else(|_| Response::new(Body::empty()))
|
||||
@@ -845,7 +870,7 @@ pub async fn sidebar_filetree_command_runtime_asset() -> Response {
|
||||
header::CONTENT_TYPE,
|
||||
"application/javascript; charset=utf-8",
|
||||
)
|
||||
.header(header::CACHE_CONTROL, "no-store")
|
||||
.header(header::CACHE_CONTROL, runtime_asset_cache_control())
|
||||
.header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
||||
.body(Body::from(JS))
|
||||
.unwrap_or_else(|_| Response::new(Body::empty()))
|
||||
@@ -859,7 +884,7 @@ pub async fn sidebar_filetree_upload_runtime_asset() -> Response {
|
||||
header::CONTENT_TYPE,
|
||||
"application/javascript; charset=utf-8",
|
||||
)
|
||||
.header(header::CACHE_CONTROL, "no-store")
|
||||
.header(header::CACHE_CONTROL, runtime_asset_cache_control())
|
||||
.header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
||||
.body(Body::from(JS))
|
||||
.unwrap_or_else(|_| Response::new(Body::empty()))
|
||||
@@ -873,7 +898,7 @@ pub async fn sidebar_attachment_open_runtime_asset() -> Response {
|
||||
header::CONTENT_TYPE,
|
||||
"application/javascript; charset=utf-8",
|
||||
)
|
||||
.header(header::CACHE_CONTROL, "no-store")
|
||||
.header(header::CACHE_CONTROL, runtime_asset_cache_control())
|
||||
.header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
||||
.body(Body::from(JS))
|
||||
.unwrap_or_else(|_| Response::new(Body::empty()))
|
||||
@@ -887,7 +912,7 @@ pub async fn filetree_keyboard_runtime_asset() -> Response {
|
||||
header::CONTENT_TYPE,
|
||||
"application/javascript; charset=utf-8",
|
||||
)
|
||||
.header(header::CACHE_CONTROL, "no-store")
|
||||
.header(header::CACHE_CONTROL, runtime_asset_cache_control())
|
||||
.header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
||||
.body(Body::from(JS))
|
||||
.unwrap_or_else(|_| Response::new(Body::empty()))
|
||||
@@ -901,7 +926,7 @@ pub async fn filetree_dnd_runtime_asset() -> Response {
|
||||
header::CONTENT_TYPE,
|
||||
"application/javascript; charset=utf-8",
|
||||
)
|
||||
.header(header::CACHE_CONTROL, "no-store")
|
||||
.header(header::CACHE_CONTROL, runtime_asset_cache_control())
|
||||
.header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
||||
.body(Body::from(JS))
|
||||
.unwrap_or_else(|_| Response::new(Body::empty()))
|
||||
@@ -915,7 +940,7 @@ pub async fn filetree_context_menu_runtime_asset() -> Response {
|
||||
header::CONTENT_TYPE,
|
||||
"application/javascript; charset=utf-8",
|
||||
)
|
||||
.header(header::CACHE_CONTROL, "no-store")
|
||||
.header(header::CACHE_CONTROL, runtime_asset_cache_control())
|
||||
.header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
||||
.body(Body::from(JS))
|
||||
.unwrap_or_else(|_| Response::new(Body::empty()))
|
||||
@@ -929,7 +954,7 @@ pub async fn filetree_runtime_asset() -> Response {
|
||||
header::CONTENT_TYPE,
|
||||
"application/javascript; charset=utf-8",
|
||||
)
|
||||
.header(header::CACHE_CONTROL, "no-store")
|
||||
.header(header::CACHE_CONTROL, runtime_asset_cache_control())
|
||||
.header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
||||
.body(Body::from(JS))
|
||||
.unwrap_or_else(|_| Response::new(Body::empty()))
|
||||
@@ -943,7 +968,7 @@ pub async fn filetree_selection_runtime_asset() -> Response {
|
||||
header::CONTENT_TYPE,
|
||||
"application/javascript; charset=utf-8",
|
||||
)
|
||||
.header(header::CACHE_CONTROL, "no-store")
|
||||
.header(header::CACHE_CONTROL, runtime_asset_cache_control())
|
||||
.header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
||||
.body(Body::from(JS))
|
||||
.unwrap_or_else(|_| Response::new(Body::empty()))
|
||||
@@ -957,7 +982,7 @@ pub async fn tree_live_controller_runtime_asset() -> Response {
|
||||
header::CONTENT_TYPE,
|
||||
"application/javascript; charset=utf-8",
|
||||
)
|
||||
.header(header::CACHE_CONTROL, "no-store")
|
||||
.header(header::CACHE_CONTROL, runtime_asset_cache_control())
|
||||
.header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
||||
.body(Body::from(JS))
|
||||
.unwrap_or_else(|_| Response::new(Body::empty()))
|
||||
@@ -971,7 +996,7 @@ pub async fn tree_shell_runtime_asset() -> Response {
|
||||
header::CONTENT_TYPE,
|
||||
"application/javascript; charset=utf-8",
|
||||
)
|
||||
.header(header::CACHE_CONTROL, "no-store")
|
||||
.header(header::CACHE_CONTROL, runtime_asset_cache_control())
|
||||
.header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
||||
.body(Body::from(JS))
|
||||
.unwrap_or_else(|_| Response::new(Body::empty()))
|
||||
@@ -985,7 +1010,7 @@ pub async fn tree_shell_render_runtime_asset() -> Response {
|
||||
header::CONTENT_TYPE,
|
||||
"application/javascript; charset=utf-8",
|
||||
)
|
||||
.header(header::CACHE_CONTROL, "no-store")
|
||||
.header(header::CACHE_CONTROL, runtime_asset_cache_control())
|
||||
.header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
||||
.body(Body::from(JS))
|
||||
.unwrap_or_else(|_| Response::new(Body::empty()))
|
||||
@@ -999,7 +1024,7 @@ pub async fn tree_shell_page_runtime_asset() -> Response {
|
||||
header::CONTENT_TYPE,
|
||||
"application/javascript; charset=utf-8",
|
||||
)
|
||||
.header(header::CACHE_CONTROL, "no-store")
|
||||
.header(header::CACHE_CONTROL, runtime_asset_cache_control())
|
||||
.header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
||||
.body(Body::from(JS))
|
||||
.unwrap_or_else(|_| Response::new(Body::empty()))
|
||||
@@ -1013,7 +1038,7 @@ pub async fn tree_shell_state_runtime_asset() -> Response {
|
||||
header::CONTENT_TYPE,
|
||||
"application/javascript; charset=utf-8",
|
||||
)
|
||||
.header(header::CACHE_CONTROL, "no-store")
|
||||
.header(header::CACHE_CONTROL, runtime_asset_cache_control())
|
||||
.header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
||||
.body(Body::from(JS))
|
||||
.unwrap_or_else(|_| Response::new(Body::empty()))
|
||||
@@ -1027,7 +1052,7 @@ pub async fn tree_shell_icons_runtime_asset() -> Response {
|
||||
header::CONTENT_TYPE,
|
||||
"application/javascript; charset=utf-8",
|
||||
)
|
||||
.header(header::CACHE_CONTROL, "no-store")
|
||||
.header(header::CACHE_CONTROL, runtime_asset_cache_control())
|
||||
.header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
||||
.body(Body::from(JS))
|
||||
.unwrap_or_else(|_| Response::new(Body::empty()))
|
||||
@@ -1041,7 +1066,7 @@ pub async fn tree_shell_filetree_runtime_asset() -> Response {
|
||||
header::CONTENT_TYPE,
|
||||
"application/javascript; charset=utf-8",
|
||||
)
|
||||
.header(header::CACHE_CONTROL, "no-store")
|
||||
.header(header::CACHE_CONTROL, runtime_asset_cache_control())
|
||||
.header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
||||
.body(Body::from(JS))
|
||||
.unwrap_or_else(|_| Response::new(Body::empty()))
|
||||
@@ -1055,7 +1080,7 @@ pub async fn tree_shell_filetree_menu_runtime_asset() -> Response {
|
||||
header::CONTENT_TYPE,
|
||||
"application/javascript; charset=utf-8",
|
||||
)
|
||||
.header(header::CACHE_CONTROL, "no-store")
|
||||
.header(header::CACHE_CONTROL, runtime_asset_cache_control())
|
||||
.header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
||||
.body(Body::from(JS))
|
||||
.unwrap_or_else(|_| Response::new(Body::empty()))
|
||||
@@ -1069,7 +1094,7 @@ pub async fn tree_shell_filetree_dnd_runtime_asset() -> Response {
|
||||
header::CONTENT_TYPE,
|
||||
"application/javascript; charset=utf-8",
|
||||
)
|
||||
.header(header::CACHE_CONTROL, "no-store")
|
||||
.header(header::CACHE_CONTROL, runtime_asset_cache_control())
|
||||
.header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
||||
.body(Body::from(JS))
|
||||
.unwrap_or_else(|_| Response::new(Body::empty()))
|
||||
@@ -1083,7 +1108,7 @@ pub async fn tree_shell_picker_runtime_asset() -> Response {
|
||||
header::CONTENT_TYPE,
|
||||
"application/javascript; charset=utf-8",
|
||||
)
|
||||
.header(header::CACHE_CONTROL, "no-store")
|
||||
.header(header::CACHE_CONTROL, runtime_asset_cache_control())
|
||||
.header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
||||
.body(Body::from(JS))
|
||||
.unwrap_or_else(|_| Response::new(Body::empty()))
|
||||
@@ -1097,7 +1122,7 @@ pub async fn tree_shell_dom_runtime_asset() -> Response {
|
||||
header::CONTENT_TYPE,
|
||||
"application/javascript; charset=utf-8",
|
||||
)
|
||||
.header(header::CACHE_CONTROL, "no-store")
|
||||
.header(header::CACHE_CONTROL, runtime_asset_cache_control())
|
||||
.header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
||||
.body(Body::from(JS))
|
||||
.unwrap_or_else(|_| Response::new(Body::empty()))
|
||||
@@ -1112,7 +1137,7 @@ pub async fn document_conflict_panel_runtime_asset() -> Response {
|
||||
header::CONTENT_TYPE,
|
||||
"application/javascript; charset=utf-8",
|
||||
)
|
||||
.header(header::CACHE_CONTROL, "no-store")
|
||||
.header(header::CACHE_CONTROL, runtime_asset_cache_control())
|
||||
.header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
||||
.body(Body::from(JS))
|
||||
.unwrap_or_else(|_| Response::new(Body::empty()))
|
||||
@@ -1126,7 +1151,7 @@ pub async fn document_pane_runtime_asset() -> Response {
|
||||
header::CONTENT_TYPE,
|
||||
"application/javascript; charset=utf-8",
|
||||
)
|
||||
.header(header::CACHE_CONTROL, "no-store")
|
||||
.header(header::CACHE_CONTROL, runtime_asset_cache_control())
|
||||
.header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
||||
.body(Body::from(JS))
|
||||
.unwrap_or_else(|_| Response::new(Body::empty()))
|
||||
@@ -1140,7 +1165,7 @@ pub async fn document_mindmap_host_runtime_asset() -> Response {
|
||||
header::CONTENT_TYPE,
|
||||
"application/javascript; charset=utf-8",
|
||||
)
|
||||
.header(header::CACHE_CONTROL, "no-store")
|
||||
.header(header::CACHE_CONTROL, runtime_asset_cache_control())
|
||||
.header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
||||
.body(Body::from(JS))
|
||||
.unwrap_or_else(|_| Response::new(Body::empty()))
|
||||
@@ -1154,7 +1179,7 @@ pub async fn document_resource_tab_runtime_asset() -> Response {
|
||||
header::CONTENT_TYPE,
|
||||
"application/javascript; charset=utf-8",
|
||||
)
|
||||
.header(header::CACHE_CONTROL, "no-store")
|
||||
.header(header::CACHE_CONTROL, runtime_asset_cache_control())
|
||||
.header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
||||
.body(Body::from(JS))
|
||||
.unwrap_or_else(|_| Response::new(Body::empty()))
|
||||
@@ -1168,7 +1193,7 @@ pub async fn document_session_runtime_asset() -> Response {
|
||||
header::CONTENT_TYPE,
|
||||
"application/javascript; charset=utf-8",
|
||||
)
|
||||
.header(header::CACHE_CONTROL, "no-store")
|
||||
.header(header::CACHE_CONTROL, runtime_asset_cache_control())
|
||||
.header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
||||
.body(Body::from(JS))
|
||||
.unwrap_or_else(|_| Response::new(Body::empty()))
|
||||
@@ -1182,7 +1207,7 @@ pub async fn document_slash_position_runtime_asset() -> Response {
|
||||
header::CONTENT_TYPE,
|
||||
"application/javascript; charset=utf-8",
|
||||
)
|
||||
.header(header::CACHE_CONTROL, "no-store")
|
||||
.header(header::CACHE_CONTROL, runtime_asset_cache_control())
|
||||
.header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
||||
.body(Body::from(JS))
|
||||
.unwrap_or_else(|_| Response::new(Body::empty()))
|
||||
@@ -1196,7 +1221,7 @@ pub async fn document_tiptap_conversion_runtime_asset() -> Response {
|
||||
header::CONTENT_TYPE,
|
||||
"application/javascript; charset=utf-8",
|
||||
)
|
||||
.header(header::CACHE_CONTROL, "no-store")
|
||||
.header(header::CACHE_CONTROL, runtime_asset_cache_control())
|
||||
.header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
||||
.body(Body::from(JS))
|
||||
.unwrap_or_else(|_| Response::new(Body::empty()))
|
||||
@@ -1210,7 +1235,7 @@ pub async fn document_editor_adapter_runtime_asset() -> Response {
|
||||
header::CONTENT_TYPE,
|
||||
"application/javascript; charset=utf-8",
|
||||
)
|
||||
.header(header::CACHE_CONTROL, "no-store")
|
||||
.header(header::CACHE_CONTROL, runtime_asset_cache_control())
|
||||
.header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
||||
.body(Body::from(JS))
|
||||
.unwrap_or_else(|_| Response::new(Body::empty()))
|
||||
@@ -1228,6 +1253,10 @@ pub async fn leptos_tiptap_manifest() -> Response {
|
||||
});
|
||||
let mut response = Json(manifest).into_response();
|
||||
stamp_shell_headers(response.headers_mut(), "leptos-tiptap-runtime");
|
||||
response.headers_mut().insert(
|
||||
header::CACHE_CONTROL,
|
||||
HeaderValue::from_static(runtime_asset_cache_control()),
|
||||
);
|
||||
response
|
||||
}
|
||||
|
||||
@@ -1251,7 +1280,7 @@ pub async fn leptos_tiptap_asset(Path(asset_path): Path<String>) -> Result<Respo
|
||||
header::CONTENT_TYPE,
|
||||
runtime_asset_content_type(&asset_path),
|
||||
)
|
||||
.header(header::CACHE_CONTROL, "no-store")
|
||||
.header(header::CACHE_CONTROL, runtime_asset_cache_control())
|
||||
.header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
||||
.body(Body::from(bytes))
|
||||
.map_err(|error| WebError::internal(format!("runtime asset 响应构造失败: {error}")))?;
|
||||
@@ -1312,7 +1341,24 @@ pub(crate) async fn build_page_aggregate_snapshot(
|
||||
})?;
|
||||
ensure_local_workspace_read_access_with_state(state, context, root_uri)
|
||||
.map_err(|error| error.with_context(context))?;
|
||||
return resolve_local_markdown_page_aggregate(root_uri, document_id);
|
||||
let root_uri_for_build = root_uri.to_string();
|
||||
let root_uri_for_preferences = root_uri_for_build.clone();
|
||||
let document_id = document_id.to_string();
|
||||
let mut aggregate = tokio::task::spawn_blocking(move || {
|
||||
#[cfg(test)]
|
||||
block_local_page_aggregate_for_test();
|
||||
resolve_local_markdown_page_aggregate(&root_uri_for_build, &document_id)
|
||||
})
|
||||
.await
|
||||
.map_err(|error| WebError::internal(format!("本地 Page Aggregate 构建任务失败: {error}")))??;
|
||||
super::ui_preferences::apply_effective_page_preferences(
|
||||
state,
|
||||
context,
|
||||
&mut aggregate,
|
||||
source_kind,
|
||||
Some(root_uri_for_preferences.as_str()),
|
||||
)?;
|
||||
return Ok(aggregate);
|
||||
}
|
||||
|
||||
let meta = load_document_meta_result(
|
||||
@@ -1398,6 +1444,7 @@ pub(crate) fn escape_script_json(value: &str) -> String {
|
||||
}
|
||||
|
||||
pub(crate) async fn load_workspace_shell_projection(
|
||||
state: Option<&AppState>,
|
||||
config: &AppConfig,
|
||||
context: &RequestContext,
|
||||
workspace_id: &str,
|
||||
@@ -1412,7 +1459,7 @@ pub(crate) async fn load_workspace_shell_projection(
|
||||
query: None,
|
||||
max_results: None,
|
||||
};
|
||||
let dataset = match load_projection_snapshot(config, context, &spec).await {
|
||||
let mut dataset = match load_projection_snapshot(config, context, &spec).await {
|
||||
Ok(snapshot) => snapshot.dataset,
|
||||
Err(_) if config.allow_dev_fixtures => {
|
||||
let documents = active_document_id
|
||||
@@ -1444,6 +1491,9 @@ pub(crate) async fn load_workspace_shell_projection(
|
||||
"degraded_reason": "projection_unavailable"
|
||||
}),
|
||||
};
|
||||
if let Some(state) = state {
|
||||
attach_sidebar_shortcuts_to_dataset(state, context, workspace_id, &mut dataset);
|
||||
}
|
||||
|
||||
build_workspace_shell_projection(
|
||||
&dataset,
|
||||
@@ -1453,6 +1503,25 @@ pub(crate) async fn load_workspace_shell_projection(
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn attach_sidebar_shortcuts_to_dataset(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
workspace_id: &str,
|
||||
dataset: &mut Value,
|
||||
) {
|
||||
let shortcuts = crate::routes::sidebar_shortcuts::load_sidebar_shortcut_dataset(
|
||||
state,
|
||||
context,
|
||||
workspace_id,
|
||||
);
|
||||
if shortcuts.is_empty() {
|
||||
return;
|
||||
}
|
||||
if let Some(object) = dataset.as_object_mut() {
|
||||
object.insert("sidebarShortcuts".to_string(), Value::Array(shortcuts));
|
||||
}
|
||||
}
|
||||
|
||||
/// 加载侧栏页面树 HTML(SSR)
|
||||
///
|
||||
/// 从 sidebar projection snapshot 中构建页面树 HTML 字符串。
|
||||
@@ -1605,7 +1674,23 @@ pub(crate) fn render_local_file_tree_html(
|
||||
active_document_id: Option<&str>,
|
||||
active_row_id: Option<&str>,
|
||||
) -> Result<String, WebError> {
|
||||
let snapshot = load_local_folder_file_tree_snapshot_with_reveal(root_uri, active_document_id)?;
|
||||
render_local_file_tree_html_scoped(root_uri, active_document_id, active_row_id, None)
|
||||
}
|
||||
|
||||
pub(crate) fn render_local_file_tree_html_scoped(
|
||||
root_uri: &str,
|
||||
active_document_id: Option<&str>,
|
||||
active_row_id: Option<&str>,
|
||||
file_tree_scope: Option<&str>,
|
||||
) -> Result<String, WebError> {
|
||||
let snapshot = if let Some(scope) = file_tree_scope
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
load_local_folder_file_tree_children_snapshot(root_uri, scope)?
|
||||
} else {
|
||||
load_local_folder_file_tree_snapshot_with_reveal(root_uri, active_document_id)?
|
||||
};
|
||||
let rows =
|
||||
collect_filetree_render_rows(&snapshot.projection, active_document_id, active_row_id);
|
||||
Ok(render_initial_filetree_html(&FileTreeInitialRenderInput {
|
||||
@@ -1613,14 +1698,23 @@ pub(crate) fn render_local_file_tree_html(
|
||||
}))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn block_local_page_aggregate_for_test() {
|
||||
let delay_ms = LOCAL_PAGE_AGGREGATE_TEST_BLOCK_MS.load(std::sync::atomic::Ordering::SeqCst);
|
||||
if delay_ms > 0 {
|
||||
std::thread::sleep(std::time::Duration::from_millis(delay_ms));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::app::{build_app, AppConfig, AppState};
|
||||
use crate::context::RequestContext;
|
||||
use axum::body::{to_bytes, Body};
|
||||
use axum::http::{header, HeaderMap, HeaderValue, Method, Request, StatusCode, Uri};
|
||||
use control_plane::{DirectoryGrantInput, UpsertUserInput};
|
||||
use control_plane::{DirectoryGrantInput, UpsertUserInput, UpsertUserUiPreferenceInput};
|
||||
use serde_json::Value;
|
||||
use std::time::{Duration, Instant};
|
||||
use tower::util::ServiceExt;
|
||||
|
||||
const DOCUMENT_EDITOR_ADAPTER_RUNTIME_JS: &str =
|
||||
@@ -1917,6 +2011,12 @@ mod tests {
|
||||
assert!(html.contains("data-mnote-resource-tab-host"));
|
||||
assert!(runtime.contains("openPrimaryMindmap"));
|
||||
assert!(runtime.contains("openResourceInActiveTab"));
|
||||
assert!(html.contains(
|
||||
r#"<link rel="modulepreload" href="/api/leptos-tiptap-runtime/mnote-leptos-tiptap-spike-island.js">"#
|
||||
));
|
||||
assert!(html.contains(
|
||||
r#"<link rel="preload" href="/api/leptos-tiptap-runtime/mnote-leptos-tiptap-spike-island_bg.wasm" as="fetch" type="application/wasm" crossorigin>"#
|
||||
));
|
||||
assert!(resource_runtime.contains("/api/local-folder/resource/read"));
|
||||
assert!(resource_runtime.contains("/api/local-folder/resource/write"));
|
||||
let mindmap_runtime = DOCUMENT_MINDMAP_HOST_RUNTIME_JS;
|
||||
@@ -2033,6 +2133,78 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn mnote_browser_runtime_assets_are_cacheable() {
|
||||
let response = app()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/api/mnote-browser-runtime/sidebar-tree-runtime.js")
|
||||
.body(Body::empty())
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let cache_control = response
|
||||
.headers()
|
||||
.get(header::CACHE_CONTROL)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.expect("cache-control");
|
||||
assert_ne!(cache_control, "no-store");
|
||||
assert!(
|
||||
cache_control.contains("max-age"),
|
||||
"runtime 资产应允许浏览器缓存,避免远程高延迟时每次重拉"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn leptos_tiptap_runtime_assets_are_cacheable() {
|
||||
let manifest_response = app()
|
||||
.clone()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/api/leptos-tiptap-runtime/manifest.json")
|
||||
.body(Body::empty())
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
assert_eq!(manifest_response.status(), StatusCode::OK);
|
||||
let manifest_cache_control = manifest_response
|
||||
.headers()
|
||||
.get(header::CACHE_CONTROL)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.expect("manifest cache-control");
|
||||
assert_ne!(manifest_cache_control, "no-store");
|
||||
assert!(
|
||||
manifest_cache_control.contains("max-age"),
|
||||
"leptos-tiptap manifest 应允许浏览器缓存,避免每次重新发现 runtime 入口"
|
||||
);
|
||||
|
||||
let response = app()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/api/leptos-tiptap-runtime/mnote-leptos-tiptap-spike-island.js")
|
||||
.body(Body::empty())
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let cache_control = response
|
||||
.headers()
|
||||
.get(header::CACHE_CONTROL)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.expect("cache-control");
|
||||
assert_ne!(cache_control, "no-store");
|
||||
assert!(
|
||||
cache_control.contains("max-age"),
|
||||
"leptos-tiptap runtime 资产应允许浏览器缓存,避免远程高延迟时每次重拉"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn page_aggregate_endpoint_errors_without_convex_or_fixture() {
|
||||
let response = app_with_unreachable_convex_without_fixture()
|
||||
@@ -2243,6 +2415,190 @@ mod tests {
|
||||
assert!(aggregate.body.content.to_string().contains("Grant Heading"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn local_page_aggregate_does_not_block_leptos_runtime_asset_request() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"mnote-local-page-aggregate-runtime-asset-{}",
|
||||
std::process::id()
|
||||
));
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
std::fs::create_dir_all(root.join("Cold Start")).expect("create local page bundle");
|
||||
std::fs::write(
|
||||
root.join("Cold Start").join("Cold Start.md"),
|
||||
"# Cold Start\n\nEditor cold start target.\n",
|
||||
)
|
||||
.expect("write local md");
|
||||
|
||||
let root_uri = format!("file://{}", root.display());
|
||||
init_local_workspace(&root, "user_test");
|
||||
let app = app();
|
||||
let aggregate_app = app.clone();
|
||||
let aggregate_uri = format!(
|
||||
"/api/page-aggregate/local-md:Cold~20Start~2FCold~20Start.md?sourceKind=local_folder&rootUri={root_uri}"
|
||||
);
|
||||
super::LOCAL_PAGE_AGGREGATE_TEST_BLOCK_MS
|
||||
.store(400, std::sync::atomic::Ordering::SeqCst);
|
||||
struct ResetLocalAggregateBlock;
|
||||
impl Drop for ResetLocalAggregateBlock {
|
||||
fn drop(&mut self) {
|
||||
super::LOCAL_PAGE_AGGREGATE_TEST_BLOCK_MS
|
||||
.store(0, std::sync::atomic::Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
let _reset_block = ResetLocalAggregateBlock;
|
||||
let aggregate_task = tokio::spawn(async move {
|
||||
aggregate_app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri(aggregate_uri)
|
||||
.header("x-mnote-actor-id", "user_test")
|
||||
.header("x-mnote-actor-type", "user")
|
||||
.body(Body::empty())
|
||||
.expect("aggregate request"),
|
||||
)
|
||||
.await
|
||||
.expect("aggregate response")
|
||||
});
|
||||
|
||||
let started = Instant::now();
|
||||
tokio::task::yield_now().await;
|
||||
let asset_response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/api/leptos-tiptap-runtime/mnote-leptos-tiptap-spike-island.js")
|
||||
.body(Body::empty())
|
||||
.expect("asset request"),
|
||||
)
|
||||
.await
|
||||
.expect("asset response");
|
||||
let asset_elapsed = started.elapsed();
|
||||
let aggregate_response = aggregate_task.await.expect("aggregate task");
|
||||
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
|
||||
assert_eq!(asset_response.status(), StatusCode::OK);
|
||||
assert_eq!(aggregate_response.status(), StatusCode::OK);
|
||||
assert!(
|
||||
asset_elapsed < Duration::from_millis(150),
|
||||
"leptos-tiptap runtime asset 不应被本地 Page Aggregate 冷构建阻塞,实际等待 {asset_elapsed:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn local_folder_page_aggregate_prefers_sqlite_ui_preference_over_default_title_header() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"mnote-local-page-aggregate-ui-pref-{}",
|
||||
std::process::id()
|
||||
));
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
std::fs::create_dir_all(&root).expect("create local root");
|
||||
std::fs::write(root.join("README.md"), "# Readme Heading\n正文\n").expect("write local md");
|
||||
|
||||
let root_uri = format!("file://{}", root.display());
|
||||
let state = AppState::new(AppConfig {
|
||||
service_name: "mnote-web".into(),
|
||||
service_version: "0.1.0".into(),
|
||||
bind_addr: "127.0.0.1:0".into(),
|
||||
public_bind_addr: "127.0.0.1:3000".into(),
|
||||
legacy_next_base_url: Some("http://127.0.0.1:3100".into()),
|
||||
enable_legacy_next_compat: true,
|
||||
enable_debug_shell_routes: false,
|
||||
enable_editor_actor: true,
|
||||
hermes_base_path: "/api/hermes".into(),
|
||||
compat_next_base_path: "/api/compat/next".into(),
|
||||
convex_url: None,
|
||||
convex_admin_key: None,
|
||||
allow_dev_fixtures: true,
|
||||
query_fixtures_json: None,
|
||||
mutation_fixtures_json: None,
|
||||
dev_user_id: "dev-user".into(),
|
||||
dev_user_name: "开发用户".into(),
|
||||
dev_user_email: "dev@mnote.local".into(),
|
||||
});
|
||||
grant_local_workspace_read_access(&state, "user_test", &root_uri, &root);
|
||||
let workspace_id =
|
||||
crate::routes::local_folder_source::local_workspace_id_from_root_uri(&root_uri)
|
||||
.expect("workspace id");
|
||||
state
|
||||
.control_plane()
|
||||
.upsert_user_ui_preference(UpsertUserUiPreferenceInput {
|
||||
id: None,
|
||||
user_id: "user_test".into(),
|
||||
workspace_id: Some(workspace_id),
|
||||
source_kind: Some("local_folder".into()),
|
||||
scope_kind: "source_family".into(),
|
||||
scope_id: "external_local_folder".into(),
|
||||
key: "hideTitleHeader".into(),
|
||||
value_json: "false".into(),
|
||||
})
|
||||
.expect("upsert title header preference");
|
||||
|
||||
let context = request_context("user_test", "user");
|
||||
let aggregate = super::build_page_aggregate_snapshot(
|
||||
&state,
|
||||
&context,
|
||||
"local-md:README.md",
|
||||
None,
|
||||
Some("local_folder"),
|
||||
Some(&root_uri),
|
||||
)
|
||||
.await
|
||||
.expect("local page aggregate");
|
||||
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
|
||||
assert!(!aggregate.layout.page_options.hide_title_header);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ui_preferences_api_updates_and_returns_effective_page_options() {
|
||||
let app = app();
|
||||
let response = app
|
||||
.clone()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("PUT")
|
||||
.uri("/api/ui/preferences")
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(
|
||||
serde_json::json!({
|
||||
"workspaceId": "ws_demo",
|
||||
"sourceKind": "convex_workspace",
|
||||
"documentId": "doc_1",
|
||||
"updates": {
|
||||
"showHeadingNumbers": true,
|
||||
"layoutDensity": "compact"
|
||||
}
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("GET")
|
||||
.uri("/api/ui/preferences/effective?workspaceId=ws_demo&sourceKind=convex_workspace&documentId=doc_1")
|
||||
.body(Body::empty())
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let body = to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("body");
|
||||
let payload: Value = serde_json::from_slice(&body).expect("json");
|
||||
assert_eq!(payload["result"]["pageOptions"]["showHeadingNumbers"], true);
|
||||
assert_eq!(payload["result"]["pageOptions"]["layoutDensity"], "compact");
|
||||
assert_eq!(payload["result"]["sources"]["showHeadingNumbers"], "global");
|
||||
assert_eq!(payload["result"]["sources"]["layoutDensity"], "workspace");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn document_shell_renders_local_markdown_with_same_sidebar_surfaces() {
|
||||
let root =
|
||||
@@ -2469,6 +2825,7 @@ mod tests {
|
||||
let filetree_html =
|
||||
super::load_file_tree_html(&config, &context, "ws_demo", Some("doc_1"), None).await;
|
||||
let workspace_projection = super::load_workspace_shell_projection(
|
||||
None,
|
||||
&config,
|
||||
&context,
|
||||
"ws_demo",
|
||||
|
||||
Reference in New Issue
Block a user