Skip to main content

veilid_core/veilid_api/
api.rs

1use super::*;
2
3impl_veilid_log_facility!("veilid_api");
4
5/////////////////////////////////////////////////////////////////////////////////////////////////////
6
7pub(super) struct VeilidAPIInner {
8    context: Option<VeilidCoreContext>,
9    #[cfg(feature = "debug-api")]
10    pub(super) debug_cache: debug::DebugCache,
11}
12
13impl fmt::Debug for VeilidAPIInner {
14    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
15        write!(f, "VeilidAPIInner")
16    }
17}
18
19impl Drop for VeilidAPIInner {
20    fn drop(&mut self) {
21        if let Some(context) = self.context.take() {
22            spawn_detached("api shutdown", api_shutdown(context));
23        }
24    }
25}
26
27/// The primary developer entrypoint into `veilid-core` functionality.
28///
29/// From [VeilidAPI] one can access various components:
30///
31/// * [VeilidConfig] - The Veilid configuration specified at startup time.
32/// * [Crypto] - The available set of cryptosystems provided by Veilid.
33/// * [TableStore] - The Veilid table-based encrypted persistent key-value store.
34/// * [ProtectedStore] - The Veilid abstract of the device's low-level 'protected secret storage'.
35/// * [VeilidState] - The current state of the Veilid node this API accesses.
36/// * [RoutingContext] - Communication methods between Veilid nodes and private routes.
37/// * Attach and detach from the network.
38/// * Create and import private routes.
39/// * Reply to `AppCall` RPCs.
40#[derive(Clone, Debug)]
41#[must_use]
42pub struct VeilidAPI {
43    inner: Arc<Mutex<VeilidAPIInner>>,
44}
45
46impl VeilidAPI {
47    #[cfg_attr(feature = "instrument", instrument(target = "veilid_api", level = "debug", fields(duration, __VEILID_LOG_KEY = context.log_key()), skip_all))]
48    pub(crate) fn new(context: VeilidCoreContext) -> Self {
49        veilid_log!(context debug "VeilidAPI::new()");
50        record_duration(|| Self {
51            inner: Arc::new(Mutex::new(VeilidAPIInner {
52                context: Some(context),
53                #[cfg(feature = "debug-api")]
54                debug_cache: Default::default(),
55            })),
56        })
57    }
58
59    /// Shut down Veilid and terminate the API.
60    ///
61    /// Blocks until the core context has finished shutting down. Idempotent: a second call after the
62    /// context is already taken is a no-op.
63    #[cfg_attr(feature = "instrument", instrument(target = "veilid_api", level = "debug", fields(duration, __VEILID_LOG_KEY = self.log_key()), skip_all))]
64    pub async fn shutdown(self) {
65        let context = { self.inner.lock().context.take() };
66        let recorder = DurationRecorder::new("VeilidAPI::shutdown", |name, start| {
67            veilid_log!(self debug "{}[start={:#}]()", name, start);
68        });
69        recorder
70            .record_fut(
71                async {
72                    if let Some(context) = context {
73                        api_shutdown(context).await;
74                    }
75                },
76                |name, start, dur, ret| {
77                    veilid_log!(self debug "{}[start={:#} dur={:#}](ret: ())", name, start, dur);
78                    ret
79                },
80            )
81            .await
82    }
83
84    /// Check to see if Veilid is already shut down.
85    #[must_use]
86    pub fn is_shutdown(&self) -> bool {
87        self.inner.lock().context.is_none()
88    }
89
90    ////////////////////////////////////////////////////////////////
91    // Public Accessors
92
93    /// Access the configuration that Veilid was initialized with.
94    ///
95    /// Errors with [VeilidAPIError::NotInitialized] if the API has been shut down.
96    pub fn config(&self) -> VeilidAPIResult<Arc<VeilidConfig>> {
97        let inner = self.inner.lock();
98        let Some(context) = &inner.context else {
99            return Err(VeilidAPIError::NotInitialized);
100        };
101        Ok(context.registry().config())
102    }
103
104    /// Get the cryptosystem component.
105    ///
106    /// Errors with [VeilidAPIError::NotInitialized] if the API has been shut down.
107    pub fn crypto(&self) -> VeilidAPIResult<VeilidComponentGuard<'_, Crypto>> {
108        let inner = self.inner.lock();
109        let Some(context) = &inner.context else {
110            return Err(VeilidAPIError::NotInitialized);
111        };
112        context
113            .registry()
114            .lookup::<Crypto>()
115            .ok_or(VeilidAPIError::NotInitialized)
116    }
117
118    /// Get the TableStore component.
119    ///
120    /// Errors with [VeilidAPIError::NotInitialized] if the API has been shut down.
121    pub fn table_store(&self) -> VeilidAPIResult<VeilidComponentGuard<'_, TableStore>> {
122        let inner = self.inner.lock();
123        let Some(context) = &inner.context else {
124            return Err(VeilidAPIError::NotInitialized);
125        };
126        context
127            .registry()
128            .lookup::<TableStore>()
129            .ok_or(VeilidAPIError::NotInitialized)
130    }
131
132    /// Get the ProtectedStore component.
133    ///
134    /// Errors with [VeilidAPIError::NotInitialized] if the API has been shut down.
135    pub fn protected_store(&self) -> VeilidAPIResult<VeilidComponentGuard<'_, ProtectedStore>> {
136        let inner = self.inner.lock();
137        let Some(context) = &inner.context else {
138            return Err(VeilidAPIError::NotInitialized);
139        };
140        context
141            .registry()
142            .lookup::<ProtectedStore>()
143            .ok_or(VeilidAPIError::NotInitialized)
144    }
145
146    /// Get the BlockStore component.
147    ///
148    /// Errors with [VeilidAPIError::NotInitialized] if the API has been shut down.
149    #[cfg(feature = "unstable-blockstore")]
150    pub fn block_store(&self) -> VeilidAPIResult<VeilidComponentGuard<'_, BlockStore>> {
151        let inner = self.inner.lock();
152        let Some(context) = &inner.context else {
153            return Err(VeilidAPIError::NotInitialized);
154        };
155        context
156            .registry()
157            .lookup::<BlockStore>()
158            .ok_or(VeilidAPIError::NotInitialized)
159    }
160
161    ////////////////////////////////////////////////////////////////
162    // Attach/Detach
163
164    /// Get a full copy of the current state of Veilid.
165    ///
166    /// Errors with [VeilidAPIError::NotInitialized] if the API has been shut down.
167    #[expect(clippy::unused_async)]
168    pub async fn get_state(&self) -> VeilidAPIResult<VeilidState> {
169        let attachment_manager = self.core_context()?.attachment_manager();
170        let network_manager = attachment_manager.network_manager();
171        let config = self.config()?;
172
173        let attachment = attachment_manager.get_veilid_state();
174        let network = network_manager.get_veilid_state();
175
176        Ok(VeilidState {
177            attachment,
178            network,
179            config: Box::new(VeilidStateConfig {
180                config: config.as_ref().clone(),
181            }),
182        })
183    }
184
185    /// Connect to the network.
186    ///
187    /// Sets the attachment to maintain peers; the network connect proceeds in the background tick loop.
188    /// Returns an error if already attached.
189    ///
190    /// Errors with [VeilidAPIError::Generic] if already attached, or [VeilidAPIError::NotInitialized]
191    /// if the API has been shut down.
192    #[cfg_attr(feature = "instrument", instrument(target = "veilid_api", level = "debug", fields(duration, __VEILID_LOG_KEY = self.log_key()), skip_all, ret))]
193    pub async fn attach(&self) -> VeilidAPIResult<()> {
194        async {
195            let attachment_manager = self.core_context()?.attachment_manager();
196            let recorder = DurationRecorder::new("VeilidAPI::attach", |name, start| {
197                veilid_log!(self debug "{}[start={:#}]()", name, start);
198            });
199            recorder.record_fut(
200                async {
201                    if !Box::pin(attachment_manager.attach()).await {
202                        apibail_generic!("Already attached");
203                    }
204                    VeilidAPIResult::Ok(())
205                },
206                |name, start, dur, ret| {
207                    veilid_log!(self debug "{}[start={:#} dur={:#}](ret: {:?})", name, start, dur, ret);
208                    ret
209                },
210            ).await
211        }
212        .await
213        .inspect_err(log_veilid_api_error!(self))
214    }
215
216    /// Disconnect from the network.
217    ///
218    /// Clears the attachment's maintain-peers flag; the network teardown proceeds in the background tick loop.
219    /// Returns an error if already detached.
220    ///
221    /// Errors with [VeilidAPIError::Generic] if already detached, or [VeilidAPIError::NotInitialized]
222    /// if the API has been shut down.
223    #[cfg_attr(feature = "instrument", instrument(target = "veilid_api", level = "debug", fields(duration, __VEILID_LOG_KEY = self.log_key()), skip_all, ret))]
224    pub async fn detach(&self) -> VeilidAPIResult<()> {
225        async {
226            let attachment_manager = self.core_context()?.attachment_manager();
227            let recorder = DurationRecorder::new("VeilidAPI::detach", |name, start| {
228                veilid_log!(self debug "{}[start={:#}]()", name, start);
229            });
230            recorder.record_fut(
231                async {
232                    if !Box::pin(attachment_manager.detach()).await {
233                        apibail_generic!("Already detached");
234                    }
235                    VeilidAPIResult::Ok(())
236                },
237                |name, start, dur, ret| {
238                    veilid_log!(self debug "{}[start={:#} dur={:#}](ret: {:?})", name, start, dur, ret);
239                    ret
240                },
241            ).await
242        }
243        .await
244        .inspect_err(log_veilid_api_error!(self))
245    }
246
247    ////////////////////////////////////////////////////////////////
248    // Routing Context
249
250    /// Get a new `RoutingContext` object to use to send messages over the Veilid network with default safety, sequencing, and stability parameters.
251    ///
252    /// Errors with [VeilidAPIError::NotInitialized] if the API has been shut down.
253    #[cfg_attr(feature = "instrument", instrument(target = "veilid_api", level = "debug", fields(duration, __VEILID_LOG_KEY = self.log_key()), skip_all, ret))]
254    pub fn routing_context(&self) -> VeilidAPIResult<RoutingContext> {
255        record_duration(|| {
256            veilid_log!(self debug "VeilidAPI::routing_context()");
257
258            RoutingContext::try_new(self.clone())
259        })
260        .inspect_err(log_veilid_api_error!(self))
261    }
262
263    ////////////////////////////////////////////////////////////////
264    // Non-RoutingContext DHT Operations
265
266    /// Deterministicly builds the record key for a given schema and owner public key.
267    /// The crypto kind of the record key will be that of the `owner` public key
268    ///
269    /// Local crypto computation only; despite being `async` it makes no network round-trip.
270    ///
271    /// Errors with [VeilidAPIError::InvalidArgument] if `schema` is malformed, [VeilidAPIError::Generic]
272    /// if `owner_key` or `encryption_key` names an unsupported crypto kind, or
273    /// [VeilidAPIError::NotInitialized] if the API has been shut down.
274    #[cfg_attr(feature = "instrument", instrument(target = "veilid_api", level = "debug", fields(duration, __VEILID_LOG_KEY = self.log_key()), ret))]
275    pub async fn get_dht_record_key(
276        &self,
277        schema: DHTSchema,
278        owner_key: PublicKey,
279        encryption_key: Option<SharedSecret>,
280    ) -> VeilidAPIResult<RecordKey> {
281        async {
282            schema.validate()?;
283            self.crypto()?.check_public_key(&owner_key)?;
284            if let Some(encryption_key) = encryption_key.as_ref() {
285                self.crypto()?.check_shared_secret(encryption_key)?;
286            }
287            let storage_manager = self.core_context()?.storage_manager();
288
289            let recorder = DurationRecorder::new("VeilidAPI::get_dht_record_key", |name, start| {
290                veilid_log!(self debug
291                    "{}[start={:#}](self: {:?}, schema: {:?}, owner_key: {:?}, encryption_key: {:?})", name, start, self, schema, owner_key, encryption_key);
292            });
293            recorder.record_fut(
294                storage_manager.get_record_key(schema, &owner_key, encryption_key),
295                |name, start, dur, ret| {
296                    veilid_log!(self debug
297                        "{}[start={:#} dur={:#}](ret: {:?})", name, start, dur, ret);
298                    ret
299                },
300            ).await
301        }.await.inspect_err(log_veilid_api_error!(self))
302    }
303
304    /// Create a new MemberId for use with in creating `DHTSchema`s.
305    ///
306    /// Errors with [VeilidAPIError::Generic] if `writer_key` names an unsupported crypto kind, or
307    /// [VeilidAPIError::NotInitialized] if the API has been shut down.
308    #[cfg_attr(feature = "instrument", instrument(target = "veilid_api", level = "debug", skip(self), fields(duration, __VEILID_LOG_KEY = self.log_key()), ret))]
309    pub fn generate_member_id(&self, writer_key: &PublicKey) -> VeilidAPIResult<MemberId> {
310        record_duration(move || {
311            veilid_log!(self debug "VeilidAPI::generate_member_id(writer_key: {:?}", writer_key);
312
313            self.crypto()?.check_public_key(writer_key)?;
314
315            let storage_manager = self.core_context()?.storage_manager();
316            storage_manager.generate_member_id(writer_key)
317        })
318        .inspect_err(log_veilid_api_error!(self))
319    }
320
321    /// Start a transaction on a set of DHT records
322    /// Record keys must have been opened via a routing context already when passed to this function
323    /// The maximum number of records per transaction is currently 32.
324    /// Options can be specified that supply a default signing keypair for records that are not opened for writing
325    ///
326    /// Blocks on the network to begin the transaction across the record nodes (online-only).
327    /// The returned [DHTTransaction] holds a network-side resource the caller must release by calling
328    /// [DHTTransaction::commit] or [DHTTransaction::rollback]; dropping it without doing either logs a
329    /// warning and tears the transaction down in the background.
330    ///
331    /// Errors with [VeilidAPIError::InvalidArgument] if a record is not open or more than 32 records
332    /// are passed, [VeilidAPIError::MissingArgument] if `record_keys` is empty or has duplicates,
333    /// [VeilidAPIError::Generic] if a record key is malformed or its encryption key does not match the
334    /// opened record, [VeilidAPIError::TryAgain] if the DHT is offline, the records are contended, or
335    /// begin consensus was not reached (retry), [VeilidAPIError::NotInitialized] if the API has been
336    /// shut down. Network failures surface as [VeilidAPIError::Timeout] or
337    /// [VeilidAPIError::NoConnection].
338    #[cfg_attr(feature = "instrument", instrument(target = "veilid_api", level = "debug", fields(duration, __VEILID_LOG_KEY = self.log_key()), ret))]
339    pub async fn transact_dht_records(
340        &self,
341        record_keys: Vec<RecordKey>,
342        options: Option<TransactDHTRecordsOptions>,
343    ) -> VeilidAPIResult<DHTTransaction> {
344        async {
345            let storage_manager = self.core_context()?.storage_manager();
346            for record_key in &record_keys {
347                storage_manager.check_record_key(record_key)?;
348            }
349
350            let recorder = DurationRecorder::new("VeilidAPI::transact_dht_records", |name, start| {
351                veilid_log!(self debug
352                    "{}[start={:#}](self: {:?}, record_keys: {:?}, options: {:?})", name, start, self, record_keys, options);
353            });
354            recorder.record_fut(
355                async {
356                    let handle = Box::pin(storage_manager.begin_transaction(record_keys, options)).await?;
357                    DHTTransaction::new(self.clone(), handle)
358                },
359                |name, start, dur, ret| {
360                    veilid_log!(self debug
361                        "{}[start={:#} dur={:#}](ret: {:?})", name, start, dur, ret);
362                    ret
363                },
364            ).await
365        }.await.inspect_err(log_veilid_api_error!(self))
366    }
367
368    ////////////////////////////////////////////////////////////////
369    // Private route allocation
370
371    /// Allocate a new private route set with default cryptography and network options.
372    /// Default settings are for [Stability::Reliable] and [Sequencing::PreferOrdered].
373    /// Returns a route id and a publishable 'blob' with the route encrypted with each crypto kind.
374    /// Those nodes importing the blob will have their choice of which crypto kind to use.
375    ///
376    /// Returns a route id and 'blob' that can be published over some means (DHT or otherwise) to be
377    /// imported by another Veilid node.
378    ///
379    /// Blocks on the network to allocate and test the route. The returned route id holds an allocated
380    /// route the caller must free with [VeilidAPI::release_private_route].
381    ///
382    /// Errors with [VeilidAPIError::TryAgain] if there is no valid PublicInternet network class yet,
383    /// not enough nodes are known to build the route, or the route failed its reachability test
384    /// (retry), or [VeilidAPIError::NotInitialized] if the API has been shut down.
385    pub async fn new_private_route(&self) -> VeilidAPIResult<RouteBlob> {
386        Box::pin(self.new_custom_private_route(PrivateSpec::default())).await
387    }
388
389    /// Allocate a new private route and specify a specific cryptosystem, stability and sequencing preference.
390    /// Faster connections may be possible with [Stability::LowLatency], and [Sequencing::PreferUnordered] at the
391    /// expense of some loss of messages.
392    /// Returns a route id and a publishable 'blob' with the route encrypted with each crypto kind.
393    /// Those nodes importing the blob will have their choice of which crypto kind to use.
394    ///
395    /// Returns a route id and 'blob' that can be published over some means (DHT or otherwise) to be
396    /// imported by another Veilid node.
397    ///
398    /// Blocks on the network to allocate and test the route. The returned route id holds an allocated
399    /// route the caller must free with [VeilidAPI::release_private_route].
400    ///
401    /// Errors with [VeilidAPIError::Generic] if `private_spec` names an invalid crypto kind,
402    /// [VeilidAPIError::InvalidArgument] if the hop count exceeds the configured maximum,
403    /// [VeilidAPIError::TryAgain] if there is no valid PublicInternet network class yet, not enough
404    /// nodes are known to build the route, or the route failed its reachability test (retry), or
405    /// [VeilidAPIError::NotInitialized] if the API has been shut down.
406    #[cfg_attr(feature = "instrument", instrument(target = "veilid_api", level = "debug", fields(duration, __VEILID_LOG_KEY = self.log_key()), skip(self), ret))]
407    pub async fn new_custom_private_route(
408        &self,
409        mut private_spec: PrivateSpec,
410    ) -> VeilidAPIResult<RouteBlob> {
411        async {
412            let default_route_hop_count: usize =
413                self.config()?.network.rpc.default_route_hop_count.into();
414
415            if private_spec.crypto_kinds.is_empty() {
416                private_spec.crypto_kinds = VALID_CRYPTO_KINDS.to_vec();
417            } else {
418                for kind in &private_spec.crypto_kinds {
419                    Crypto::validate_crypto_kind(*kind)?;
420                }
421            }
422            if private_spec.hop_count == 0 {
423                private_spec.hop_count = default_route_hop_count;
424            }
425
426            let routing_table = self.core_context()?.routing_table();
427            let rss = routing_table.route_spec_store();
428
429            let recorder = DurationRecorder::new("VeilidAPI::new_custom_private_route", |name, start| {
430                veilid_log!(self debug "{}[start={:#}](private_spec: {:?})", name, start, private_spec);
431            });
432            recorder.record_fut(
433                async {
434                    let allocate_route_params = AllocateRouteParams {
435                        crypto_kinds: private_spec.crypto_kinds,
436                        hop_count: private_spec.hop_count,
437                        stability: private_spec.stability,
438                        sequencing: private_spec.sequencing,
439                        directions: DirectionSet::all(),
440                        avoid_nodes: Vec::new(),
441                        automatic: false,
442                    };
443                    let RouteIdAndKeys {
444                        route_id,
445                        route_set_keys: _,
446                    } = rss.allocate_route(allocate_route_params).await?;
447                    let route_id_api: RouteId = route_id.clone().into();
448                    match Box::pin(rss.test_route(route_id_api.clone())).await? {
449                        Some(true) => {}
450                        Some(false) => {
451                            rss.release_route(route_id_api.clone());
452                            apibail_try_again!("allocated route failed to test");
453                        }
454                        None => {
455                            rss.release_route(route_id_api.clone());
456                            apibail_try_again!("allocated route could not be tested");
457                        }
458                    }
459                    let private_routes = rss.assemble_private_route_set(&route_id, Some(true)).await?;
460                    let blob = match RouteSpecStore::private_routes_to_blob(&private_routes) {
461                        Ok(v) => v,
462                        Err(e) => {
463                            rss.release_route(route_id_api);
464                            return Err(e);
465                        }
466                    };
467                    rss.mark_route_published(&route_id, true)?;
468                    VeilidAPIResult::Ok(RouteBlob {
469                        route_id: route_id_api,
470                        blob: blob.into(),
471                    })
472                },
473                |name, start, dur, ret| {
474                    veilid_log!(self debug "{}[start={:#} dur={:#}](ret: {:?})", name, start, dur, ret);
475                    ret
476                },
477            ).await
478        }
479        .await
480        .inspect_err(log_veilid_api_error!(self))
481    }
482
483    /// Import a private route blob as a remote private route.
484    ///
485    /// Returns a route id that can be used to send private messages to the node creating this route.
486    ///
487    /// Local import, no network round-trip. The returned route id holds an imported route the caller
488    /// must free with [VeilidAPI::release_private_route].
489    ///
490    /// Errors with [VeilidAPIError::InvalidArgument] if `blob` is empty or names too many crypto kinds,
491    /// [VeilidAPIError::ParseError] if it is malformed, [VeilidAPIError::Generic] if the decoded route
492    /// has no first hop, or [VeilidAPIError::NotInitialized] if the API has been shut down.
493    #[cfg_attr(feature = "instrument", instrument(target = "veilid_api", level = "debug", fields(duration, __VEILID_LOG_KEY = self.log_key()), skip(self), ret))]
494    pub fn import_remote_private_route(&self, blob: Vec<u8>) -> VeilidAPIResult<RouteId> {
495        record_duration(|| {
496            veilid_log!(self debug
497                "VeilidAPI::import_remote_private_route(blob: {:?})", blob);
498            let routing_table = self.core_context()?.routing_table();
499            let rss = routing_table.route_spec_store();
500            rss.import_remote_route_blob(blob)
501        })
502        .inspect_err(log_veilid_api_error!(self))
503    }
504
505    /// Release either a locally allocated or remotely imported private route.
506    ///
507    /// This will deactivate the route and free its resources and it can no longer be sent to
508    /// or received from.
509    ///
510    /// This is the release for [VeilidAPI::new_private_route], [VeilidAPI::new_custom_private_route], and
511    /// [VeilidAPI::import_remote_private_route]. Local, no network round-trip. Releasing a route id that
512    /// is unknown, already released, or malformed (unsupported crypto kind or bad length) returns
513    /// [VeilidAPIError::InvalidArgument]; errors with [VeilidAPIError::NotInitialized] if the API has
514    /// been shut down.
515    #[cfg_attr(feature = "instrument", instrument(target = "veilid_api", level = "debug", fields(duration, __VEILID_LOG_KEY = self.log_key()), skip(self), ret))]
516    pub fn release_private_route(&self, route_id: RouteId) -> VeilidAPIResult<()> {
517        record_duration(|| {
518            veilid_log!(self debug
519                "VeilidAPI::release_private_route(route_id: {:?})", route_id);
520
521            let routing_table = self.core_context()?.routing_table();
522            routing_table.check_route_id(&route_id)?;
523
524            let rss = routing_table.route_spec_store();
525            if !rss.release_route(route_id.clone()) {
526                apibail_invalid_argument!("release_private_route", "key", route_id);
527            }
528            Ok(())
529        })
530        .inspect_err(log_veilid_api_error!(self))
531    }
532
533    ////////////////////////////////////////////////////////////////
534    // App Calls
535
536    /// Respond to an AppCall received over a [VeilidUpdate::AppCall].
537    ///
538    /// * `call_id` - specifies which call to reply to, and it comes from a [VeilidUpdate::AppCall], specifically the [VeilidAppCall::id()] value.
539    /// * `message` - is an answer blob to be returned by the remote node's [RoutingContext::app_call()] function, and may be up to 32768 bytes.
540    ///
541    /// Completes the pending call locally and does not block on the network. Each `call_id` may be
542    /// answered only once; replying to an unknown or already-answered `call_id` errors with
543    /// [VeilidAPIError::Generic]. Errors with [VeilidAPIError::TryAgain] if the node is mid-shutdown,
544    /// or [VeilidAPIError::NotInitialized] if the API has been shut down.
545    #[cfg_attr(feature = "instrument", instrument(target = "veilid_api", level = "debug", fields(duration, __VEILID_LOG_KEY = self.log_key()), skip(self), ret))]
546    pub async fn app_call_reply(
547        &self,
548        call_id: OperationId,
549        message: Vec<u8>,
550    ) -> VeilidAPIResult<()> {
551        async {
552            let rpc_processor = self.core_context()?.rpc_processor();
553
554            let message_len = message.len();
555            let recorder = DurationRecorder::new("VeilidAPI::app_call_reply", |name, start| {
556                veilid_log!(self debug
557                    "{}[start={:#}](call_id: {:?}, message_len: {})", name, start, call_id, message_len);
558                veilid_log!(self trace "message: {:?}", message);
559            });
560            recorder.record(
561                || rpc_processor
562                    .app_call_reply(call_id, message.into())
563                    .map_err(|e| e.into()),
564                |name, start, dur, ret| {
565                    veilid_log!(self debug
566                        "{}[start={:#} dur={:#}](ret: {:?})", name, start, dur, ret);
567                    ret
568                },
569            )
570        }.await.inspect_err(log_veilid_api_error!(self))
571    }
572
573    ////////////////////////////////////////////////////////////////
574    // Tunnel Building
575
576    #[cfg(feature = "unstable-tunnels")]
577    #[cfg_attr(feature = "instrument", instrument(target = "veilid_api", level = "debug", fields(duration, __VEILID_LOG_KEY = self.log_key()), skip(self), ret))]
578    pub async fn start_tunnel(
579        &self,
580        _endpoint_mode: TunnelMode,
581        _depth: u8,
582    ) -> VeilidAPIResult<PartialTunnel> {
583        apibail_internal!("unimplemented");
584    }
585
586    #[cfg(feature = "unstable-tunnels")]
587    #[cfg_attr(feature = "instrument", instrument(target = "veilid_api", level = "debug", fields(duration, __VEILID_LOG_KEY = self.log_key()), skip(self), ret))]
588    pub async fn complete_tunnel(
589        &self,
590        _endpoint_mode: TunnelMode,
591        _depth: u8,
592        _partial_tunnel: PartialTunnel,
593    ) -> VeilidAPIResult<FullTunnel> {
594        apibail_internal!("unimplemented");
595    }
596
597    #[cfg(feature = "unstable-tunnels")]
598    #[cfg_attr(feature = "instrument", instrument(target = "veilid_api", level = "debug", fields(duration, __VEILID_LOG_KEY = self.log_key()), skip(self), ret))]
599    pub async fn cancel_tunnel(&self, _tunnel_id: TunnelId) -> VeilidAPIResult<bool> {
600        apibail_internal!("unimplemented");
601    }
602
603    ////////////////////////////////////////////////////////////////
604    // Internal Accessors
605
606    pub(crate) fn core_context(&self) -> VeilidAPIResult<VeilidCoreContext> {
607        let inner = self.inner.lock();
608        let Some(context) = &inner.context else {
609            return Err(VeilidAPIError::NotInitialized);
610        };
611        Ok(context.clone())
612    }
613
614    #[cfg(feature = "debug-api")]
615    pub(crate) fn with_debug_cache<R, F: FnOnce(&mut debug::DebugCache) -> R>(
616        &self,
617        callback: F,
618    ) -> R {
619        let mut inner = self.inner.lock();
620        callback(&mut inner.debug_cache)
621    }
622
623    #[must_use]
624    pub(crate) fn log_key(&self) -> &str {
625        let inner = self.inner.lock();
626        let Some(context) = &inner.context else {
627            return "";
628        };
629        context.log_key()
630    }
631}