From 7e30589d0b028d92bd58b931bbc5b24ad832b11c Mon Sep 17 00:00:00 2001 From: Xun Lu Date: Fri, 6 Nov 2026 12:40:59 +0800 Subject: [PATCH 1/2] perf(host): export only the requested plugin package management.exportPackage loaded the whole committed source for every export: all installed packages read from SQLite, every digest recomputed and the plan validated again, under the manager's lock. The workbench exports one package per UI plugin while it starts, so each import paid for every package of the installation (25 packages, about 9 MB by default). Read only the requested package of the revision instead. A foreign source is still rejected, a package outside the revision is still reported as not installed, and the exported bytes are still checked against their digest. In the release desktop fixture the startup plugin load chain drops from about 560 ms to 260 ms, and the workspace picker appears about 170 ms sooner. Generated-by: Claude Code --- platform/event-log/src/applications.rs | 44 +++++++++++++++++ platform/event-log/src/applications/tests.rs | 47 ++++++++++++++++++- .../event-log/src/root_store/applications.rs | 11 +++++ .../src/application/management.rs | 17 ++++--- 4 files changed, 112 insertions(+), 7 deletions(-) diff --git a/platform/event-log/src/applications.rs b/platform/event-log/src/applications.rs index d20fa621b45..74fc35a81d8 100644 --- a/platform/event-log/src/applications.rs +++ b/platform/event-log/src/applications.rs @@ -326,6 +326,50 @@ impl EventLog { .await } + /// Exact bytes of one package in a committed revision, without reading or + /// validating the revision's other packages. `None` when the revision does + /// not contain that package. + pub async fn load_application_package_at( + &self, + source: &DeploymentRef, + package: &PackageRef, + ) -> Result, StoreError> { + source.validate().map_err(invalid)?; + if source.state_domain != self.application_domain()? { + return Err(invalid("foreign application state domain")); + } + package + .validate() + .map_err(|error| invalid(error.to_string()))?; + let deployment = source.deployment.to_string(); + let revision = source.revision as i64; + let package = package.clone(); + self.connection + .run(move |connection| { + Box::pin(async move { + let mut tx = connection.begin().await?; + let installed: bool = sqlx::query_scalar( + "SELECT EXISTS(SELECT 1 FROM application_source_packages + WHERE deployment = ? AND source_revision = ? AND id = ? AND revision = ?)", + ) + .bind(&deployment) + .bind(revision) + .bind(&package.id) + .bind(&package.revision) + .fetch_one(&mut *tx) + .await?; + let package = if installed { + Some(read_package(&mut tx, &package).await?) + } else { + None + }; + tx.commit().await?; + Ok(package) + }) + }) + .await + } + async fn read_metadata( &self, deployment: Uuid, diff --git a/platform/event-log/src/applications/tests.rs b/platform/event-log/src/applications/tests.rs index f9615a5fff0..a3a032392ab 100644 --- a/platform/event-log/src/applications/tests.rs +++ b/platform/event-log/src/applications/tests.rs @@ -142,12 +142,48 @@ async fn immutable_bytes_preserve_every_committed_source_revision() { .files(), old.files() ); + assert_eq!( + store + .load_application_package_at(&original, &old.application_ref()) + .await + .unwrap() + .unwrap() + .files(), + old.files() + ); + let current = store + .load_application(a) + .await + .unwrap() + .unwrap() + .source + .reference; + assert!( + store + .load_application_package_at(¤t, &old.application_ref()) + .await + .unwrap() + .is_none() + ); let mut absent = original.clone(); absent.revision = 3; assert!(store.load_application_at(&absent).await.unwrap().is_none()); + assert!( + store + .load_application_package_at(&absent, &old.application_ref()) + .await + .unwrap() + .is_none() + ); let mut foreign = original; foreign.state_domain = "0".repeat(64); assert!(store.load_application_at(&foreign).await.is_err()); + assert!( + store + .load_application_package_at(&foreign, &old.application_ref()) + .await + .is_err() + ); store.close().await.unwrap(); } @@ -156,7 +192,7 @@ async fn corrupted_exact_bytes_cannot_be_loaded_or_silently_overwritten() { let (_dir, store) = store().await; let id = Uuid::new_v4(); let package = package("export default {};"); - store + let source = store .commit_application(id, 0, selected(&package), vec![], vec![package.clone()]) .await .unwrap(); @@ -183,6 +219,15 @@ async fn corrupted_exact_bytes_cannot_be_loaded_or_silently_overwritten() { .to_string() .contains("digest mismatch") ); + assert!( + store + .load_application_package_at(&source, &package.application_ref()) + .await + .err() + .unwrap() + .to_string() + .contains("digest mismatch") + ); assert!( store .commit_application(id, 1, selected(&package), vec![], vec![package]) diff --git a/platform/event-log/src/root_store/applications.rs b/platform/event-log/src/root_store/applications.rs index f35758c0c04..dbdd092d89f 100644 --- a/platform/event-log/src/root_store/applications.rs +++ b/platform/event-log/src/root_store/applications.rs @@ -29,6 +29,7 @@ use maka_plugin_core::{ composition::{Input, InstanceId}, deployment::{DeploymentRef, RemoteObservation, management::ManagedDeployment}, operation::RecoveryRef, + package::PackageRef, }; use serde_json::Value; use uuid::Uuid; @@ -72,6 +73,16 @@ impl RootStore { self.require_open()?; self.control.load_application_at(reference).await } + pub async fn load_application_package_at( + &self, + source: &DeploymentRef, + package: &PackageRef, + ) -> Result, StoreError> { + self.require_open()?; + self.control + .load_application_package_at(source, package) + .await + } pub async fn read_application_metadata( &self, deployment: Uuid, diff --git a/platform/host-runtime/src/application/management.rs b/platform/host-runtime/src/application/management.rs index 2dc76dc1bb0..4f34ff18ecb 100644 --- a/platform/host-runtime/src/application/management.rs +++ b/platform/host-runtime/src/application/management.rs @@ -410,14 +410,19 @@ impl Manager { async fn export_package(&self, input: Value, context: Context) -> Result { let request: PackageBundleRead = serde_json::from_value(input).map_err(|error| error.to_string())?; + if request.source.state_domain != self.source.state_domain + || request.source.deployment != self.source.deployment + { + return Err("foreign managed deployment source".into()); + } + // The UI exports each plugin's package while it starts. Loading the whole + // source here would read and hash every installed package per export. let package = { let _read = self.writes.lock().await; - let loaded = self.source_at(Some(request.source)).await?; - loaded - .packages() - .iter() - .find(|package| package.application_ref() == request.package) - .cloned() + self.store + .load_application_package_at(&request.source, &request.package) + .await + .map_err(|error| error.to_string())? .ok_or("package is not installed in this source")? }; let mut ticket = context From 776b1b75056a4fe5d65e4bdfbeb9c5fc9fbbd6fc Mon Sep 17 00:00:00 2001 From: M4n5ter Date: Fri, 6 Nov 2026 17:37:46 +0800 Subject: [PATCH 2/2] perf(host): isolate single-package reads and move package validation off SQLite lane --- platform/event-log/src/applications.rs | 126 ++++++---- platform/event-log/src/applications/tests.rs | 112 ++++++++- .../src/application/management.rs | 24 +- .../src/application/management/authoring.rs | 14 +- .../src/application/management/composition.rs | 33 ++- .../src/application/management/tests.rs | 124 +++++++++- .../plugin-core/src/deployment/management.rs | 233 +++++++++--------- 7 files changed, 473 insertions(+), 193 deletions(-) diff --git a/platform/event-log/src/applications.rs b/platform/event-log/src/applications.rs index 74fc35a81d8..846b218d6b2 100644 --- a/platform/event-log/src/applications.rs +++ b/platform/event-log/src/applications.rs @@ -343,8 +343,9 @@ impl EventLog { .map_err(|error| invalid(error.to_string()))?; let deployment = source.deployment.to_string(); let revision = source.revision as i64; - let package = package.clone(); - self.connection + let requested = package.clone(); + let files = self + .connection .run(move |connection| { Box::pin(async move { let mut tx = connection.begin().await?; @@ -354,20 +355,31 @@ impl EventLog { ) .bind(&deployment) .bind(revision) - .bind(&package.id) - .bind(&package.revision) + .bind(&requested.id) + .bind(&requested.revision) .fetch_one(&mut *tx) .await?; - let package = if installed { - Some(read_package(&mut tx, &package).await?) + let files = if installed { + Some(read_package_files(&mut tx, &requested).await?) } else { None }; tx.commit().await?; - Ok(package) + Ok(files) }) }) + .await?; + let Some(files) = files else { + return Ok(None); + }; + let package = package.clone(); + tokio::task::spawn_blocking(move || build_checked_package(files, &package).map(Some)) .await + .map_err(|error| { + invalid(format!( + "application package validation worker failed: {error}" + )) + })? } async fn read_metadata( @@ -401,7 +413,8 @@ impl EventLog { return Err(invalid("invalid deployment")); } let owner = std::sync::Arc::downgrade(self.root_owner.as_ref().expect("owned source Root")); - self.connection + let raw = self + .connection .run(move |connection| { Box::pin(async move { let mut tx = connection.begin().await?; @@ -410,28 +423,43 @@ impl EventLog { else { return Ok(None); }; - let mut packages = Vec::new(); - for reference in metadata.packages { - let package = read_package(&mut tx, &reference).await?; - packages.push(package); + let mut raw_packages = Vec::with_capacity(metadata.packages.len()); + for reference in &metadata.packages { + let files = read_package_files(&mut tx, reference).await?; + raw_packages.push((reference.clone(), files)); } - let plan = validate( - &metadata.source.input, - &metadata.source.remote_exports, - &packages, - )?; tx.commit().await?; - let snapshot = - SourceSnapshot::new(owner, metadata.source.reference.clone(), &plan); - Ok(Some(LoadedDeployment { - snapshot, - source: metadata.source, - packages, - plan, - })) + Ok(Some((metadata, raw_packages))) }) }) - .await + .await?; + let Some((metadata, raw_packages)) = raw else { + return Ok(None); + }; + tokio::task::spawn_blocking(move || { + let mut packages = Vec::with_capacity(raw_packages.len()); + for (reference, files) in raw_packages { + packages.push(build_checked_package(files, &reference)?); + } + let plan = validate( + &metadata.source.input, + &metadata.source.remote_exports, + &packages, + )?; + let snapshot = SourceSnapshot::new(owner, metadata.source.reference.clone(), &plan); + Ok(Some(LoadedDeployment { + snapshot, + source: metadata.source, + packages, + plan, + })) + }) + .await + .map_err(|error| { + invalid(format!( + "application source validation worker failed: {error}" + )) + })? } } @@ -505,32 +533,38 @@ async fn read_metadata( })) } -async fn read_package( +async fn read_package_files( connection: &mut SqliteConnection, reference: &PackageRef, -) -> Result { - let rows = sqlx::query("SELECT path, - CASE WHEN typeof(payload) = 'blob' THEN length(payload) END AS bytes FROM application_package_files WHERE id = ? AND revision = ? ORDER BY path") - .bind(&reference.id).bind(&reference.revision).fetch_all(&mut *connection).await?; +) -> Result>, StoreError> { + let rows = sqlx::query( + "SELECT path, + CASE WHEN typeof(payload) = 'blob' THEN payload END AS payload + FROM application_package_files WHERE id = ? AND revision = ? ORDER BY path", + ) + .bind(&reference.id) + .bind(&reference.revision) + .fetch_all(&mut *connection) + .await?; if rows.is_empty() { return Err(invalid("invalid stored application package file count")); } - for row in &rows { - usize::try_from( - row.try_get::, _>("bytes")? - .ok_or_else(|| invalid("stored package payload must be a BLOB"))?, - ) - .map_err(|_| invalid("invalid package payload length"))?; - let path: &str = row.try_get("path")?; - maka_packages::validate_path(path).map_err(|e| invalid(e.to_string()))?; - } let mut files = BTreeMap::new(); for row in rows { let path: String = row.try_get("path")?; - let bytes: Vec = sqlx::query_scalar("SELECT payload FROM application_package_files WHERE id = ? AND revision = ? AND path = ?") - .bind(&reference.id).bind(&reference.revision).bind(&path).fetch_one(&mut *connection).await?; + maka_packages::validate_path(&path).map_err(|e| invalid(e.to_string()))?; + let bytes: Vec = row + .try_get::>, _>("payload")? + .ok_or_else(|| invalid("stored package payload must be a BLOB"))?; files.insert(path, bytes); } + Ok(files) +} + +fn build_checked_package( + files: BTreeMap>, + reference: &PackageRef, +) -> Result { let package = Package::new(files).map_err(|e| invalid(e.to_string()))?; if &package.application_ref() != reference { return Err(invalid("application package digest mismatch")); @@ -538,5 +572,13 @@ async fn read_package( Ok(package) } +async fn read_package( + connection: &mut SqliteConnection, + reference: &PackageRef, +) -> Result { + let files = read_package_files(connection, reference).await?; + build_checked_package(files, reference) +} + #[cfg(test)] mod tests; diff --git a/platform/event-log/src/applications/tests.rs b/platform/event-log/src/applications/tests.rs index a3a032392ab..5dd2d40288b 100644 --- a/platform/event-log/src/applications/tests.rs +++ b/platform/event-log/src/applications/tests.rs @@ -262,7 +262,8 @@ async fn stored_package_paths_types_and_immutable_digests_are_checked() { let (_dir, store) = store().await; let id = Uuid::new_v4(); let package = package("export default {};"); - store + let reference = package.application_ref(); + let source = store .commit_application(id, 0, selected(&package), vec![], vec![package]) .await .unwrap(); @@ -276,18 +277,113 @@ async fn stored_package_paths_types_and_immutable_digests_are_checked() { }) .await .unwrap(); - let error = store.load_application(id).await.err().unwrap().to_string(); - if corruption.contains("char(0)") { - assert!(error.contains("payload must be a BLOB"), "{error}"); - } else if corruption.contains("zeroblob") { - assert!(error.contains("digest mismatch"), "{error}"); - } else { - assert!(error.contains("invalid package path"), "{error}"); + for error in [ + store.load_application(id).await.err().unwrap().to_string(), + store + .load_application_package_at(&source, &reference) + .await + .err() + .unwrap() + .to_string(), + ] { + if corruption.contains("char(0)") { + assert!(error.contains("payload must be a BLOB"), "{error}"); + } else if corruption.contains("zeroblob") { + assert!(error.contains("digest mismatch"), "{error}"); + } else { + assert!(error.contains("invalid package path"), "{error}"); + } } store.close().await.unwrap(); } } +#[tokio::test] +async fn single_package_load_does_not_read_or_validate_sibling_packages() { + let (_dir, store) = store().await; + let id = Uuid::new_v4(); + let intact = package("export const intact = true;"); + let sibling = Package::new(BTreeMap::from([ + ( + maka_packages::MANIFEST_FILE.into(), + serde_json::to_vec(&serde_json::json!({ + "schemaVersion": 1, + "id": "sibling", + "application": intact.manifest().application, + })) + .unwrap(), + ), + ("host.mjs".into(), b"export const sibling = true;".to_vec()), + ])) + .unwrap(); + let sibling_ref = sibling.application_ref(); + let input: Input = serde_json::from_value(serde_json::json!({ + "instances": { + "intact": { + "key": Uuid::from_u128(10), + "package": { "source": "pinned", "package": intact.application_ref() }, + "usePackage": "host" + }, + "sibling": { + "key": Uuid::from_u128(11), + "package": { "source": "pinned", "package": sibling_ref }, + "usePackage": "host" + } + } + })) + .unwrap(); + let source = store + .commit_application(id, 0, input, vec![], vec![intact.clone(), sibling]) + .await + .unwrap(); + let corrupted_id = sibling_ref.id.clone(); + let corrupted_revision = sibling_ref.revision.clone(); + store + .connection + .run(move |conn| { + Box::pin(async move { + sqlx::query( + "UPDATE application_package_files SET payload = X'00' WHERE id = ? AND revision = ? AND path = 'host.mjs'", + ) + .bind(corrupted_id) + .bind(corrupted_revision) + .execute(conn) + .await?; + Ok(()) + }) + }) + .await + .unwrap(); + assert!( + store + .load_application_at(&source) + .await + .err() + .unwrap() + .to_string() + .contains("digest mismatch") + ); + assert!( + store + .load_application_package_at(&source, &sibling_ref) + .await + .err() + .unwrap() + .to_string() + .contains("digest mismatch") + ); + assert_eq!( + store + .load_application_package_at(&source, &intact.application_ref()) + .await + .unwrap() + .unwrap() + .files(), + intact.files() + ); + store.close().await.unwrap(); +} + #[tokio::test] async fn concurrent_cas_has_one_commit_and_reopen_keeps_original_domain() { let (dir, store) = store().await; diff --git a/platform/host-runtime/src/application/management.rs b/platform/host-runtime/src/application/management.rs index 4f34ff18ecb..d8e90cc162c 100644 --- a/platform/host-runtime/src/application/management.rs +++ b/platform/host-runtime/src/application/management.rs @@ -410,26 +410,20 @@ impl Manager { async fn export_package(&self, input: Value, context: Context) -> Result { let request: PackageBundleRead = serde_json::from_value(input).map_err(|error| error.to_string())?; - if request.source.state_domain != self.source.state_domain - || request.source.deployment != self.source.deployment - { - return Err("foreign managed deployment source".into()); - } - // The UI exports each plugin's package while it starts. Loading the whole - // source here would read and hash every installed package per export. - let package = { - let _read = self.writes.lock().await; - self.store - .load_application_package_at(&request.source, &request.package) - .await - .map_err(|error| error.to_string())? - .ok_or("package is not installed in this source")? - }; let mut ticket = context .scope() .resources .reserve() .map_err(|error| error.to_string())?; + // The UI exports each plugin's package while it starts. Loading the whole + // source here would read and hash every installed package per export. + let package = self + .package_at( + &request.source, + &request.package, + "package is not installed in this source", + ) + .await?; ticket.start(); let bytes = tokio::task::spawn_blocking(move || { let bytes = package.to_bundle(); diff --git a/platform/host-runtime/src/application/management/authoring.rs b/platform/host-runtime/src/application/management/authoring.rs index b70488dc4e8..89bf60a7466 100644 --- a/platform/host-runtime/src/application/management/authoring.rs +++ b/platform/host-runtime/src/application/management/authoring.rs @@ -65,13 +65,13 @@ impl Manager { pub(super) async fn read_package(&self, input: Value) -> Result { let request: PackageSourceRead = serde_json::from_value(input).map_err(|e| e.to_string())?; - let _read = self.writes.lock().await; - let loaded = self.source_at(Some(request.source)).await?; - let package = loaded - .packages() - .iter() - .find(|p| p.application_ref() == request.package) - .ok_or("package is not installed in this source")?; + let package = self + .package_at( + &request.source, + &request.package, + "package is not installed in this source", + ) + .await?; let paths = package.files().keys().cloned().collect(); let (text, next) = if let Some(path) = request.path { let text = std::str::from_utf8(package.file(&path).ok_or("package file unavailable")?) diff --git a/platform/host-runtime/src/application/management/composition.rs b/platform/host-runtime/src/application/management/composition.rs index 7f4d0ef159c..f7ca5b257bc 100644 --- a/platform/host-runtime/src/application/management/composition.rs +++ b/platform/host-runtime/src/application/management/composition.rs @@ -164,6 +164,23 @@ impl Manager { .map_err(|e| e.to_string())? .ok_or_else(|| "managed source not found".into()) } + pub(super) async fn package_at( + &self, + source: &DeploymentRef, + package: &PackageRef, + unavailable: &'static str, + ) -> Result { + if source.state_domain != self.source.state_domain + || source.deployment != self.source.deployment + { + return Err("foreign managed deployment source".into()); + } + self.store + .load_application_package_at(source, package) + .await + .map_err(|error| error.to_string())? + .ok_or_else(|| unavailable.into()) + } pub(super) async fn binding_candidates(&self, input: Value) -> Result { let request: BindingCandidatesRead = serde_json::from_value(input).map_err(|e| e.to_string())?; @@ -340,9 +357,19 @@ impl Manager { let request: PackageSettingsRead = serde_json::from_value(input).map_err(|e| e.to_string())?; settings::check_path(&request.path, true)?; - let _read = self.writes.lock().await; - let loaded = self.source_at(Some(request.source.clone())).await?; - let declaration = declaration(loaded.packages(), &request.package)?; + let package = self + .package_at( + &request.source, + &request.package, + "application package unavailable", + ) + .await?; + let declaration = package + .manifest() + .application + .as_ref() + .map(|declaration| declaration.declaration()) + .ok_or("application package unavailable")?; let root_schema = package_schema(declaration, request.use_package)?; let mut configuration = defaults(&root_schema); if !request.changes.is_empty() { diff --git a/platform/host-runtime/src/application/management/tests.rs b/platform/host-runtime/src/application/management/tests.rs index c2e5c8aea09..267ec45fdf5 100644 --- a/platform/host-runtime/src/application/management/tests.rs +++ b/platform/host-runtime/src/application/management/tests.rs @@ -260,7 +260,7 @@ fn subscribe(manager: Manager, method: &str, input: Value) -> (Subscription, Sco #[tokio::test] async fn ui_catalog_tracks_enabled_instances_at_the_original_revision() { - tokio::time::timeout(Duration::from_secs(10), async { + tokio::time::timeout(Duration::from_secs(60), async { let fixture = Fixture::new().await; let installed = fixture .manager @@ -610,17 +610,53 @@ async fn invalid_bundles_documents_and_stale_replacements_leave_source_unchanged #[tokio::test] async fn historical_binary_bundle_export_round_trips_and_cancels_with_cleanup() { - tokio::time::timeout(Duration::from_secs(10), async { + tokio::time::timeout(Duration::from_secs(60), async { let fixture = Fixture::new().await; let package = package((0..400_000).map(|index| (index % 256) as u8).collect()); + let sibling = Package::new(BTreeMap::from([ + ( + "maka.extension.json".into(), + serde_json::to_vec(&json!({ + "schemaVersion": 1, + "id": "example.sibling", + "application": { + "host": { + "implementation": { "kind": "native", "implementation": "example.sibling" }, + "configuration": { "type": "object" }, + "imports": {}, + "exports": {} + } + } + })) + .unwrap(), + ), + ("sibling.txt".into(), b"sibling".to_vec()), + ])) + .unwrap(); + let sibling_ref = sibling.application_ref(); + let historical_input: Input = serde_json::from_value(json!({ + "instances": { + "payload": { + "key": Uuid::from_u128(1), + "package": { "source": "pinned", "package": package.application_ref() }, + "usePackage": "host" + }, + "sibling": { + "key": Uuid::from_u128(2), + "package": { "source": "pinned", "package": sibling_ref }, + "usePackage": "host" + } + } + })) + .unwrap(); let historical = fixture .store .commit_application( fixture.source.deployment, 1, - input(&package), + historical_input, vec![], - vec![package.clone()], + vec![package.clone(), sibling], ) .await .unwrap(); @@ -635,6 +671,86 @@ async fn historical_binary_bundle_export_round_trips_and_cancels_with_cleanup() ) .await .unwrap(); + // Corrupt the sibling package on disk so any accidental full-source load + // during single-package queries fails immediately at the storage boundary. + let Fixture { + manager, + store, + source, + input, + _directory: directory, + } = fixture; + drop(manager); + store.shutdown().await.unwrap(); + drop(store); + { + use sqlx::Connection as _; + let db_path = directory + .path() + .join("root") + .join(maka_event_log::root::ROOT_DATABASE); + let mut conn = sqlx::SqliteConnection::connect_with( + &sqlx::sqlite::SqliteConnectOptions::new().filename(&db_path), + ) + .await + .unwrap(); + sqlx::query( + "UPDATE application_package_files SET payload = X'00' WHERE id = ? AND revision = ? AND path = 'sibling.txt'", + ) + .bind(&sibling_ref.id) + .bind(&sibling_ref.revision) + .execute(&mut conn) + .await + .unwrap(); + conn.close().await.unwrap(); + } + let owner = Arc::new( + RootOwner::open( + &directory.path().join("root"), + &RootNamespaces { + ownership: directory.path().join("owners"), + control: directory.path().join("control"), + }, + ) + .unwrap(), + ); + let store = Arc::new(RootStore::open(owner).await.unwrap()); + let manager = ManagementTarget::from_source(store.clone(), source.clone()) + .await + .unwrap() + .0; + let fixture = Fixture { + manager, + store, + source, + input, + _directory: directory, + }; + assert!(fixture.store.load_application_at(&historical).await.is_err()); + let read_chunk = fixture + .manager + .read_package(json!({ + "source": historical, + "package": package.application_ref(), + "path": "maka.extension.json", + "offset": 0 + })) + .await + .unwrap(); + assert!(read_chunk["text"].as_str().unwrap().contains("example.payload")); + let settings_page = fixture + .manager + .package_settings(json!({ + "source": historical, + "package": package.application_ref(), + "usePackage": "host", + "changes": [], + "path": [], + "after": null + })) + .await + .unwrap(); + assert_eq!(settings_page["package"], serde_json::to_value(package.application_ref()).unwrap()); let (mut stream, scope) = export( fixture.manager.clone(), historical.clone(), diff --git a/platform/plugin-core/src/deployment/management.rs b/platform/plugin-core/src/deployment/management.rs index 2db18bee084..42f5241029b 100644 --- a/platform/plugin-core/src/deployment/management.rs +++ b/platform/plugin-core/src/deployment/management.rs @@ -263,126 +263,131 @@ pub enum ManagementRejection { } pub fn deployment_manager_contract() -> Contract { - fn method(mode: MethodMode) -> Method { - Method { - progress: None, - mode, - input: serde_json::to_value(schemars::schema_for!(I)) - .expect("management input schema") - .into(), - output: serde_json::to_value(schemars::schema_for!(O)) - .expect("management output schema") - .into(), - resources: BTreeMap::new(), + static CONTRACT: std::sync::LazyLock = std::sync::LazyLock::new(|| { + fn method(mode: MethodMode) -> Method { + Method { + progress: None, + mode, + input: serde_json::to_value(schemars::schema_for!(I)) + .expect("management input schema") + .into(), + output: serde_json::to_value(schemars::schema_for!(O)) + .expect("management output schema") + .into(), + resources: BTreeMap::new(), + } } - } - let mut install = - method::>(MethodMode::Mutation); - install - .resources - .insert("package".into(), binary_resource_type().required()); - let mut replace = - method::>(MethodMode::Mutation); - replace.resources.insert( - "document".into(), - crate::contract::text_resource_type().required(), - ); - // Optional JSON array of existing package bundle objects. The document's - // package references select the exact resulting set in the same CAS commit. - replace - .resources - .insert("packages".into(), binary_resource_type().optional()); - Contract { - name: "maka.application.DeploymentManager".into(), - version: 1, - transport: Transport::Remote, - interface: Interface::Service { - methods: BTreeMap::from([ - ( - "read".into(), - method::(MethodMode::Query), - ), - ( - "readUi".into(), - method::(MethodMode::Query), - ), - ( - "watch".into(), - method::(MethodMode::Subscription), - ), - ("install".into(), install), - ( - "installUrl".into(), - method::>( - MethodMode::Mutation, + let mut install = + method::>(MethodMode::Mutation); + install + .resources + .insert("package".into(), binary_resource_type().required()); + let mut replace = + method::>(MethodMode::Mutation); + replace.resources.insert( + "document".into(), + crate::contract::text_resource_type().required(), + ); + // Optional JSON array of existing package bundle objects. The document's + // package references select the exact resulting set in the same CAS commit. + replace + .resources + .insert("packages".into(), binary_resource_type().optional()); + Contract { + name: "maka.application.DeploymentManager".into(), + version: 1, + transport: Transport::Remote, + interface: Interface::Service { + methods: BTreeMap::from([ + ( + "read".into(), + method::(MethodMode::Query), ), - ), - ( - "installSource".into(), - method::>( - MethodMode::Mutation, + ( + "readUi".into(), + method::(MethodMode::Query), ), - ), - ( - "readPackage".into(), - method::(MethodMode::Query), - ), - ( - "exportPackage".into(), - method::(MethodMode::Subscription), - ), - ( - "replaceSource".into(), - method::>( - MethodMode::Mutation, + ( + "watch".into(), + method::(MethodMode::Subscription), ), - ), - ( - "bindingCandidates".into(), - method::(MethodMode::Query), - ), - ( - "packageSettings".into(), - method::(MethodMode::Query), - ), - ( - "editComposition".into(), - method::>( - MethodMode::Mutation, + ("install".into(), install), + ( + "installUrl".into(), + method::>( + MethodMode::Mutation, + ), ), - ), - ( - "readSettings".into(), - method::(MethodMode::Query), - ), - ( - "configure".into(), - method::>( - MethodMode::Mutation, + ( + "installSource".into(), + method::>( + MethodMode::Mutation, + ), ), - ), - ( - "configureBatch".into(), - method::>( - MethodMode::Mutation, + ( + "readPackage".into(), + method::(MethodMode::Query), ), - ), - ( - "readDocument".into(), - method::(MethodMode::Query), - ), - ( - "instances".into(), - method::(MethodMode::Query), - ), - ( - "setEnabled".into(), - method::>( - MethodMode::Mutation, + ( + "exportPackage".into(), + method::(MethodMode::Subscription), ), - ), - ("replace".into(), replace), - ]), - }, - } + ( + "replaceSource".into(), + method::>( + MethodMode::Mutation, + ), + ), + ( + "bindingCandidates".into(), + method::(MethodMode::Query), + ), + ( + "packageSettings".into(), + method::(MethodMode::Query), + ), + ( + "editComposition".into(), + method::>( + MethodMode::Mutation, + ), + ), + ( + "readSettings".into(), + method::(MethodMode::Query), + ), + ( + "configure".into(), + method::>( + MethodMode::Mutation, + ), + ), + ( + "configureBatch".into(), + method::>( + MethodMode::Mutation, + ), + ), + ( + "readDocument".into(), + method::( + MethodMode::Query, + ), + ), + ( + "instances".into(), + method::(MethodMode::Query), + ), + ( + "setEnabled".into(), + method::>( + MethodMode::Mutation, + ), + ), + ("replace".into(), replace), + ]), + }, + } + }); + CONTRACT.clone() }