Skip to main content

canic_core/api/template/
mod.rs

1use crate::{
2    cdk::types::Principal,
3    dto::{
4        error::Error,
5        template::{
6            TemplateChunkInput, TemplateChunkResponse, TemplateChunkSetInfoResponse,
7            TemplateChunkSetPrepareInput, TemplateManifestInput, WasmStoreAdminCommand,
8            WasmStoreAdminResponse, WasmStoreCatalogEntryResponse,
9            WasmStorePublicationStateResponse, WasmStoreRetiredStoreStatusResponse,
10            WasmStoreStatusResponse,
11        },
12    },
13    ids::{CanisterRole, TemplateId, TemplateVersion, WasmStoreBinding, WasmStoreGcStatus},
14    ops::{
15        config::ConfigOps,
16        ic::IcOps,
17        runtime::template::WasmStoreCatalogOps,
18        storage::{
19            state::subnet::SubnetStateOps,
20            template::{TemplateManifestOps, WasmStoreGcExecutionStats, WasmStoreLimits},
21        },
22    },
23    workflow::runtime::template::WasmStorePublicationWorkflow,
24};
25
26const ROOT_WASM_STORE_BOOTSTRAP_TEMPLATE_ID: TemplateId = TemplateId::new("embedded:wasm_store");
27const ROOT_WASM_STORE_BOOTSTRAP_BINDING: WasmStoreBinding = WasmStoreBinding::new("bootstrap");
28
29///
30/// EmbeddedTemplateApi
31///
32
33pub struct EmbeddedTemplateApi;
34
35impl EmbeddedTemplateApi {
36    // Seed approved manifests and template-keyed embedded payloads for the current release set.
37    pub fn import_embedded_release_set(wasms: &'static [(CanisterRole, &[u8])]) {
38        WasmStorePublicationWorkflow::import_embedded_release_set(wasms);
39    }
40}
41
42///
43/// WasmStoreBootstrapApi
44///
45
46pub struct WasmStoreBootstrapApi;
47
48impl WasmStoreBootstrapApi {
49    // Validate that one staged template request targets the root-local WasmStore bootstrap source.
50    fn ensure_root_wasm_store_bootstrap_template(template_id: &TemplateId) -> Result<(), Error> {
51        if template_id == &ROOT_WASM_STORE_BOOTSTRAP_TEMPLATE_ID {
52            Ok(())
53        } else {
54            Err(Error::invalid(format!(
55                "bootstrap only accepts template '{ROOT_WASM_STORE_BOOTSTRAP_TEMPLATE_ID}'"
56            )))
57        }
58    }
59
60    // Normalize one staged manifest onto the root-local WasmStore bootstrap source of truth.
61    fn normalize_root_wasm_store_bootstrap_manifest(
62        request: TemplateManifestInput,
63    ) -> Result<TemplateManifestInput, Error> {
64        if request.role != CanisterRole::WASM_STORE {
65            return Err(Error::invalid(format!(
66                "bootstrap only accepts role '{}'",
67                CanisterRole::WASM_STORE
68            )));
69        }
70
71        Self::ensure_root_wasm_store_bootstrap_template(&request.template_id)?;
72
73        let now_secs = IcOps::now_secs();
74
75        Ok(TemplateManifestInput {
76            template_id: ROOT_WASM_STORE_BOOTSTRAP_TEMPLATE_ID,
77            role: CanisterRole::WASM_STORE,
78            version: request.version,
79            payload_hash: request.payload_hash,
80            payload_size_bytes: request.payload_size_bytes,
81            store_binding: ROOT_WASM_STORE_BOOTSTRAP_BINDING,
82            chunking_mode: crate::ids::TemplateChunkingMode::Chunked,
83            manifest_state: crate::ids::TemplateManifestState::Approved,
84            approved_at: Some(now_secs),
85            created_at: now_secs,
86        })
87    }
88
89    // Seed the compact embedded release catalog used for root manifest bootstrap.
90    pub fn import_embedded_release_catalog(entries: Vec<WasmStoreCatalogEntryResponse>) {
91        WasmStoreCatalogOps::import_embedded(entries);
92    }
93
94    // Stage the normalized root-local bootstrap manifest for `embedded:wasm_store`.
95    pub fn stage_root_wasm_store_manifest(request: TemplateManifestInput) -> Result<(), Error> {
96        Self::stage_manifest(Self::normalize_root_wasm_store_bootstrap_manifest(request)?);
97        Ok(())
98    }
99
100    // Prepare root-local chunk metadata for the staged `embedded:wasm_store` bootstrap source.
101    pub fn prepare_root_wasm_store_chunk_set(
102        request: TemplateChunkSetPrepareInput,
103    ) -> Result<TemplateChunkSetInfoResponse, Error> {
104        Self::ensure_root_wasm_store_bootstrap_template(&request.template_id)?;
105        Self::prepare_chunk_set(request)
106    }
107
108    // Publish one root-local chunk into the staged `embedded:wasm_store` bootstrap source.
109    pub fn publish_root_wasm_store_chunk(request: TemplateChunkInput) -> Result<(), Error> {
110        Self::ensure_root_wasm_store_bootstrap_template(&request.template_id)?;
111        Self::publish_chunk(request)
112    }
113
114    // Stage one approved manifest in the current canister's local bootstrap source.
115    pub fn stage_manifest(input: TemplateManifestInput) {
116        TemplateManifestOps::replace_approved_from_input(input);
117    }
118
119    // Prepare one local chunk set for chunk-by-chunk staging in the current canister.
120    pub fn prepare_chunk_set(
121        request: TemplateChunkSetPrepareInput,
122    ) -> Result<TemplateChunkSetInfoResponse, Error> {
123        let now_secs = IcOps::now_secs();
124        TemplateManifestOps::prepare_chunk_set_from_input(request, now_secs).map_err(Error::from)
125    }
126
127    // Stage one chunk into the current canister's local bootstrap source.
128    pub fn publish_chunk(request: TemplateChunkInput) -> Result<(), Error> {
129        TemplateManifestOps::publish_chunk_from_input(request).map_err(Error::from)
130    }
131}
132
133///
134/// WasmStorePublicationApi
135///
136
137pub struct WasmStorePublicationApi;
138
139impl WasmStorePublicationApi {
140    // Execute one typed root-owned WasmStore publication or lifecycle admin command.
141    pub async fn admin(cmd: WasmStoreAdminCommand) -> Result<WasmStoreAdminResponse, Error> {
142        WasmStorePublicationWorkflow::handle_admin(cmd)
143            .await
144            .map_err(Error::from)
145    }
146
147    // Publish the current release set into one subnet-local wasm store.
148    pub async fn publish_current_release_set_to_store(store_pid: Principal) -> Result<(), Error> {
149        WasmStorePublicationWorkflow::publish_current_release_set_to_store(store_pid)
150            .await
151            .map_err(Error::from)
152    }
153
154    // Publish the current release set into the current subnet's selected publication wasm store.
155    pub async fn publish_current_release_set_to_current_store() -> Result<(), Error> {
156        WasmStorePublicationWorkflow::publish_current_release_set_to_current_store()
157            .await
158            .map_err(Error::from)
159    }
160
161    // Persist one explicit publication binding for the current subnet.
162    pub fn set_current_publication_store_binding(binding: WasmStoreBinding) -> Result<(), Error> {
163        WasmStorePublicationWorkflow::set_current_publication_store_binding(binding)
164            .map_err(Error::from)
165    }
166
167    // Clear the explicit publication binding for the current subnet.
168    pub fn clear_current_publication_store_binding() {
169        WasmStorePublicationWorkflow::clear_current_publication_store_binding();
170    }
171
172    // Retire the current detached publication binding for the current subnet.
173    #[must_use]
174    pub fn retire_detached_publication_store_binding() -> Option<WasmStoreBinding> {
175        WasmStorePublicationWorkflow::retire_detached_publication_store_binding()
176    }
177
178    // Return the current publication-store lifecycle state for the current subnet.
179    #[must_use]
180    pub fn publication_store_state() -> WasmStorePublicationStateResponse {
181        SubnetStateOps::publication_store_state_response()
182    }
183
184    // Return retired-store GC planning status for the current subnet, if any store is retired.
185    pub async fn retired_publication_store_status()
186    -> Result<Option<WasmStoreRetiredStoreStatusResponse>, Error> {
187        WasmStorePublicationWorkflow::retired_publication_store_status()
188            .await
189            .map_err(Error::from)
190    }
191
192    // Mark the current retired publication store as prepared for store-local GC execution.
193    pub async fn prepare_retired_publication_store_for_gc()
194    -> Result<Option<WasmStoreBinding>, Error> {
195        WasmStorePublicationWorkflow::prepare_retired_publication_store_for_gc()
196            .await
197            .map_err(Error::from)
198    }
199
200    // Mark the current retired publication store as actively executing store-local GC.
201    pub async fn begin_retired_publication_store_gc() -> Result<Option<WasmStoreBinding>, Error> {
202        WasmStorePublicationWorkflow::begin_retired_publication_store_gc()
203            .await
204            .map_err(Error::from)
205    }
206
207    // Mark the current retired publication store as having completed its local GC pass.
208    pub async fn complete_retired_publication_store_gc() -> Result<Option<WasmStoreBinding>, Error>
209    {
210        WasmStorePublicationWorkflow::complete_retired_publication_store_gc()
211            .await
212            .map_err(Error::from)
213    }
214
215    // Clear the current retired publication binding after the local store GC run has completed.
216    pub async fn finalize_retired_publication_store_binding()
217    -> Result<Option<(WasmStoreBinding, Principal)>, Error> {
218        WasmStorePublicationWorkflow::finalize_retired_publication_store_binding()
219            .await
220            .map_err(Error::from)
221    }
222
223    // Delete one finalized retired publication store after root publication state no longer references it.
224    pub async fn delete_finalized_publication_store(
225        binding: WasmStoreBinding,
226        store_pid: Principal,
227    ) -> Result<(), Error> {
228        WasmStorePublicationWorkflow::delete_finalized_publication_store(binding, store_pid)
229            .await
230            .map_err(Error::from)
231    }
232}
233
234///
235/// WasmStoreApi
236///
237
238pub struct WasmStoreApi;
239
240impl WasmStoreApi {
241    fn current_store_limits() -> Result<WasmStoreLimits, Error> {
242        let store = ConfigOps::current_wasm_store()?;
243
244        Ok(WasmStoreLimits {
245            max_store_bytes: store.max_store_bytes(),
246            max_templates: store.max_templates(),
247            max_template_versions_per_template: store.max_template_versions_per_template(),
248        })
249    }
250
251    fn current_store_headroom_bytes() -> Result<Option<u64>, Error> {
252        Ok(ConfigOps::current_wasm_store()?.headroom_bytes())
253    }
254
255    // Import the embedded template release set into this local store canister.
256    pub fn import_embedded_release_set(wasms: &'static [(CanisterRole, &[u8])]) {
257        WasmStorePublicationWorkflow::import_embedded_release_set_to_local_store(wasms);
258    }
259
260    // Return the approved template release catalog for this local store.
261    pub fn template_catalog() -> Result<Vec<WasmStoreCatalogEntryResponse>, Error> {
262        Ok(TemplateManifestOps::approved_catalog_response())
263    }
264
265    // Return current occupied-byte, retention, and store-local GC state for this local wasm store.
266    pub fn template_status(gc: WasmStoreGcStatus) -> Result<WasmStoreStatusResponse, Error> {
267        Ok(TemplateManifestOps::store_status_response(
268            Self::current_store_limits()?,
269            Self::current_store_headroom_bytes()?,
270            gc,
271        ))
272    }
273
274    // Prepare deterministic chunk-set metadata before chunk-by-chunk publication begins.
275    pub fn prepare_chunk_set(
276        request: TemplateChunkSetPrepareInput,
277    ) -> Result<TemplateChunkSetInfoResponse, Error> {
278        let now_secs = IcOps::now_secs();
279        TemplateManifestOps::prepare_chunk_set_in_store_from_input(
280            request,
281            now_secs,
282            Self::current_store_limits()?,
283        )
284        .map_err(Error::from)
285    }
286
287    // Publish one deterministic chunk into an already prepared local template release.
288    pub fn publish_chunk(request: TemplateChunkInput) -> Result<(), Error> {
289        TemplateManifestOps::publish_chunk_in_store_from_input(
290            request,
291            Self::current_store_limits()?,
292        )
293        .map_err(Error::from)
294    }
295
296    // Clear all local template metadata and chunk bytes for store-local GC execution.
297    pub async fn execute_local_store_gc() -> Result<WasmStoreGcExecutionStats, Error> {
298        TemplateManifestOps::execute_local_store_gc()
299            .await
300            .map_err(Error::from)
301    }
302
303    // Return deterministic chunk-set metadata for one local template release.
304    pub fn template_info(
305        template_id: TemplateId,
306        version: TemplateVersion,
307    ) -> Result<TemplateChunkSetInfoResponse, Error> {
308        TemplateManifestOps::chunk_set_info_response(&template_id, &version).map_err(Error::from)
309    }
310
311    // Return one deterministic chunk for one local template release.
312    pub fn template_chunk(
313        template_id: TemplateId,
314        version: TemplateVersion,
315        chunk_index: u32,
316    ) -> Result<TemplateChunkResponse, Error> {
317        TemplateManifestOps::chunk_response(&template_id, &version, chunk_index)
318            .map_err(Error::from)
319    }
320}