crux_core/capability/mod.rs
1//! Capabilities provide a user-friendly API to request side-effects from the shell.
2//!
3//! Typically, capabilities provide I/O and host API access. Capabilities are external to the
4//! core Crux library. Some are part of the Crux core distribution, others are expected to be built by the
5//! community. Apps can also build single-use capabilities where necessary.
6//!
7//! # Example use
8//!
9//! A typical use of a capability would look like the following:
10//!
11//! ```rust
12//!# use url::Url;
13//!# use crux_core::Command;
14//!# const API_URL: &str = "";
15//!# pub enum Event { Increment, Set(crux_http::Result<crux_http::Response<usize>>) }
16//!# #[derive(crux_core::macros::Effect)]
17//!# pub struct Capabilities {
18//!# pub render: crux_core::render::Render<Event>,
19//!# pub http: crux_http::Http<Event>,
20//!# }
21//!# #[derive(Default)] pub struct Model { count: usize }
22//!# #[derive(Default)] pub struct App;
23//!#
24//!# impl crux_core::App for App {
25//!# type Event = Event;
26//!# type Model = Model;
27//!# type ViewModel = ();
28//!# type Capabilities = Capabilities;
29//!# type Effect = Effect;
30//! fn update(&self, event: Self::Event, model: &mut Self::Model, caps: &Self::Capabilities) -> Command<Effect, Event> {
31//! match event {
32//! //...
33//! Event::Increment => {
34//! model.count += 1;
35//! caps.render.render(); // Render capability
36//!
37//! let base = Url::parse(API_URL).unwrap();
38//! let url = base.join("/inc").unwrap();
39//! caps.http.post(url).expect_json().send(Event::Set); // HTTP client capability
40//! }
41//! Event::Set(_) => todo!(),
42//! }
43//! Command::done()
44//! }
45//!# fn view(&self, model: &Self::Model) {
46//!# unimplemented!()
47//!# }
48//!# }
49
50//! ```
51//!
52//! Capabilities don't _perform_ side-effects themselves, they request them from the Shell. As a consequence
53//! the capability calls within the `update` function **only queue up the requests**. The side-effects themselves
54//! are performed concurrently and don't block the update function.
55//!
56//! In order to use a capability, the app needs to include it in its `Capabilities` associated type and `WithContext`
57//! trait implementation (which can be provided by the `crux_core::macros::Effect` macro). For example:
58//!
59//! ```rust
60//! mod root {
61//!
62//! // An app module which can be reused in different apps
63//! mod my_app {
64//! use crux_core::{capability::CapabilityContext, App, render::{self, Render, RenderOperation}, Command, Request};
65//! use crux_core::macros::Effect;
66//! use serde::{Serialize, Deserialize};
67//!
68//! #[derive(Default)]
69//! pub struct MyApp;
70//! #[derive(Serialize, Deserialize)]
71//! pub struct Event;
72//!
73//! // The `Effect` derive macro generates an `Effect` type that is used by the
74//! // Shell to dispatch side-effect requests to the right capability implementation
75//! // (and, in some languages, checking that all necessary capabilities are implemented)
76//! #[derive(Effect)]
77//! pub struct Capabilities {
78//! pub render: Render<Event>
79//! }
80//!
81//! impl App for MyApp {
82//! type Model = ();
83//! type Event = Event;
84//! type ViewModel = ();
85//! type Capabilities = Capabilities;
86//! type Effect = Effect;
87//!
88//! fn update(&self, event: Event, model: &mut (), caps: &Capabilities) -> Command<Effect, Event> {
89//! render::render()
90//! }
91//!
92//! fn view(&self, model: &()) {
93//! ()
94//! }
95//! }
96//! }
97//! }
98//! ```
99//!
100//! # Implementing a capability
101//!
102//! Capabilities provide an interface to request side-effects. The interface has asynchronous semantics
103//! with a form of callback. A typical capability call can look like this:
104//!
105//! ```rust,ignore
106//! caps.ducks.get_in_a_row(10, Event::RowOfDucks)
107//! ```
108//!
109//! The call above translates into "Get 10 ducks in a row and return them to me using the `RowOfDucks` event".
110//! The capability's job is to translate this request into a serializable message and instruct the Shell to
111//! do the duck herding and when it receives the ducks back, wrap them in the requested event and return it
112//! to the app.
113//!
114//! We will refer to `get_in_row` in the above call as an _operation_, the `10` is an _input_, and the
115//! `Event::RowOfDucks` is an event constructor - a function, which eventually receives the row of ducks
116//! and returns a variant of the `Event` enum. Conveniently, enum tuple variants can be used as functions,
117//! and so that will be the typical use.
118//!
119//! This is what the capability implementation could look like:
120//!
121//! ```rust
122//! use crux_core::{
123//! capability::{CapabilityContext, Operation},
124//! };
125//! use crux_core::macros::Capability;
126//! use serde::{Serialize, Deserialize};
127//!
128//! // A duck
129//! #[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
130//! struct Duck;
131//!
132//! // Operations that can be requested from the Shell
133//! #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
134//! enum DuckOperation {
135//! GetInARow(usize)
136//! }
137//!
138//! // Respective outputs for those operations
139//! #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
140//! enum DuckOutput {
141//! GetInRow(Vec<Duck>)
142//! }
143//!
144//! // Link the input and output type
145//! impl Operation for DuckOperation {
146//! type Output = DuckOutput;
147//! }
148//!
149//! // The capability. Context will provide the interface to the rest of the system.
150//! #[derive(Capability)]
151//! struct Ducks<Event> {
152//! context: CapabilityContext<DuckOperation, Event>
153//! };
154//!
155//! impl<Event> Ducks<Event> {
156//! pub fn new(context: CapabilityContext<DuckOperation, Event>) -> Self {
157//! Self { context }
158//! }
159//!
160//! pub fn get_in_a_row<F>(&self, number_of_ducks: usize, event: F)
161//! where
162//! Event: 'static,
163//! F: FnOnce(Vec<Duck>) -> Event + Send + 'static,
164//! {
165//! let ctx = self.context.clone();
166//! // Start a shell interaction
167//! self.context.spawn(async move {
168//! // Instruct Shell to get ducks in a row and await the ducks
169//! let ducks = ctx.request_from_shell(DuckOperation::GetInARow(number_of_ducks)).await;
170//!
171//! // Unwrap the ducks and wrap them in the requested event
172//! // This will always succeed, as long as the Shell implementation is correct
173//! // and doesn't send the wrong output type back
174//! if let DuckOutput::GetInRow(ducks) = ducks {
175//! // Queue an app update with the ducks event
176//! ctx.update_app(event(ducks));
177//! }
178//! })
179//! }
180//! }
181//! ```
182//!
183//! The `self.context.spawn` API allows a multi-step transaction with the Shell to be performed by a capability
184//! without involving the app, until the exchange has completed. During the exchange, one or more events can
185//! be emitted (allowing a subscription or streaming like capability to be built).
186//!
187//! For Shell requests that have no output, you can use [`CapabilityContext::notify_shell`].
188//!
189//! `DuckOperation` and `DuckOutput` show how the set of operations can be extended. In simple capabilities,
190//! with a single operation, these can be structs, or simpler types. For example, the HTTP capability works directly with
191//! `HttpRequest` and `HttpResponse`.
192
193pub(crate) mod channel;
194
195mod executor;
196mod shell_request;
197mod shell_stream;
198
199use futures::{Future, Stream, StreamExt as _};
200use serde::de::DeserializeOwned;
201use std::sync::Arc;
202
203pub(crate) use channel::channel;
204pub(crate) use executor::{executor_and_spawner, QueuingExecutor};
205
206use crate::{command::CommandOutput, Command, Request};
207use channel::Sender;
208
209/// Operation trait links together input and output of a side-effect.
210///
211/// You implement `Operation` on the payload sent by the capability to the shell using [`CapabilityContext::request_from_shell`].
212///
213/// For example (from `crux_http`):
214///
215/// ```rust,ignore
216/// impl Operation for HttpRequest {
217/// type Output = HttpResponse;
218/// }
219/// ```
220pub trait Operation:
221 serde::Serialize + serde::de::DeserializeOwned + Clone + PartialEq + Send + 'static
222{
223 /// `Output` assigns the type this request results in.
224 type Output: serde::de::DeserializeOwned + Send + Unpin + 'static;
225
226 #[cfg(feature = "typegen")]
227 #[allow(clippy::missing_errors_doc)]
228 fn register_types(generator: &mut crate::typegen::TypeGen) -> crate::typegen::Result {
229 generator.register_type::<Self>()?;
230 generator.register_type::<Self::Output>()?;
231 Ok(())
232 }
233}
234
235/// A type that can be used as a capability operation, but which will never be sent to the shell.
236/// This type is useful for capabilities that don't request effects.
237/// For example, you can use this type as the Operation for a
238/// capability that just composes other capabilities.
239///
240/// e.g.
241/// ```rust
242/// # use crux_core::capability::{CapabilityContext, Never};
243/// # use crux_core::macros::Capability;
244/// #[derive(Capability)]
245/// pub struct Compose<E> {
246/// context: CapabilityContext<Never, E>,
247/// }
248/// # impl<E> Compose<E> {
249/// # pub fn new(context: CapabilityContext<Never, E>) -> Self {
250/// # Self { context }
251/// # }
252/// # }
253///
254/// ```
255#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
256pub enum Never {}
257
258/// Implement `Operation` for `Never` to allow using it as a capability operation.
259impl Operation for Never {
260 type Output = ();
261}
262
263/// Implement the `Capability` trait for your capability. This will allow
264/// mapping events when composing apps from submodules.
265///
266/// Note that this implementation can be generated by the `crux_core::macros::Capability` derive macro.
267///
268/// Example:
269///
270/// ```rust
271/// # use crux_core::{Capability, capability::{CapabilityContext, Operation}};
272/// # pub struct Http<Ev> {
273/// # context: CapabilityContext<HttpRequest, Ev>,
274/// # }
275/// # #[derive(Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)] pub struct HttpRequest;
276/// # impl Operation for HttpRequest {
277/// # type Output = ();
278/// # }
279/// # impl<Ev> Http<Ev> where Ev: 'static, {
280/// # pub fn new(context: CapabilityContext<HttpRequest, Ev>) -> Self {
281/// # Self { context }
282/// # }
283/// # }
284/// impl<Ev> Capability<Ev> for Http<Ev> {
285/// type Operation = HttpRequest;
286/// type MappedSelf<MappedEv> = Http<MappedEv>;
287///
288/// fn map_event<F, NewEvent>(&self, f: F) -> Self::MappedSelf<NewEvent>
289/// where
290/// F: Fn(NewEvent) -> Ev + Send + Sync + 'static,
291/// Ev: 'static,
292/// NewEvent: 'static,
293/// {
294/// Http::new(self.context.map_event(f))
295/// }
296/// }
297/// ```
298pub trait Capability<Ev> {
299 type Operation: Operation + DeserializeOwned;
300
301 type MappedSelf<MappedEv>;
302
303 fn map_event<F, NewEv>(&self, f: F) -> Self::MappedSelf<NewEv>
304 where
305 F: Fn(NewEv) -> Ev + Send + Sync + 'static,
306 Ev: 'static,
307 NewEv: 'static + Send;
308}
309
310/// Allows Crux to construct app's set of required capabilities, providing context
311/// they can then use to request effects and dispatch events.
312///
313/// `new_with_context` is called by Crux and should return an instance of the app's `Capabilities` type with
314/// all capabilities constructed with context passed in. Use `Context::specialize` to
315/// create an appropriate context instance with the effect constructor which should
316/// wrap the requested operations.
317///
318/// Note that this implementation can be generated by the derive macro `crux_core::macros::Effect`.
319///
320/// ```rust
321/// # #[derive(Default)]
322/// # struct App;
323/// # pub enum Event {}
324/// # #[allow(dead_code)]
325/// # pub struct Capabilities {
326/// # http: crux_http::Http<Event>,
327/// # render: crux_core::render::Render<Event>,
328/// # }
329/// # pub enum Effect {
330/// # Http(crux_core::Request<<crux_http::Http<Event> as crux_core::capability::Capability<Event>>::Operation>),
331/// # Render(crux_core::Request<<crux_core::render::Render<Event> as crux_core::capability::Capability<Event>>::Operation>),
332/// # }
333/// # #[derive(serde::Serialize)]
334/// # pub enum EffectFfi {
335/// # Http(<crux_http::Http<Event> as crux_core::capability::Capability<Event>>::Operation),
336/// # Render(<crux_core::render::Render<Event> as crux_core::capability::Capability<Event>>::Operation),
337/// # }
338/// # impl crux_core::App for App {
339/// # type Event = Event;
340/// # type Model = ();
341/// # type ViewModel = ();
342/// # type Capabilities = Capabilities;
343/// # type Effect = Effect;
344/// # fn update(&self, _event: Self::Event, _model: &mut Self::Model, _caps: &Self::Capabilities) -> crux_core::Command<Effect, Event> {
345/// # unimplemented!()
346/// # }
347/// # fn view(&self, _model: &Self::Model) -> Self::ViewModel {
348/// # unimplemented!()
349/// # }
350/// # }
351/// # impl crux_core::Effect for Effect {
352/// # type Ffi = EffectFfi;
353/// # fn serialize(self) -> (Self::Ffi, crux_core::bridge::ResolveSerialized) {
354/// # match self {
355/// # Effect::Http(request) => request.serialize(EffectFfi::Http),
356/// # Effect::Render(request) => request.serialize(EffectFfi::Render),
357/// # }
358/// # }
359/// # }
360/// impl crux_core::WithContext<Event, Effect> for Capabilities {
361/// fn new_with_context(
362/// context: crux_core::capability::ProtoContext<Effect, Event>,
363/// ) -> Capabilities {
364/// Capabilities {
365/// http: crux_http::Http::new(context.specialize(Effect::Http)),
366/// render: crux_core::render::Render::new(context.specialize(Effect::Render)),
367/// }
368/// }
369/// }
370/// ```
371pub trait WithContext<Ev, Ef> {
372 fn new_with_context(context: ProtoContext<Ef, Ev>) -> Self;
373}
374
375impl<Event, Effect> WithContext<Event, Effect> for () {
376 fn new_with_context(_context: ProtoContext<Effect, Event>) -> Self {}
377}
378
379/// An interface for capabilities to interact with the app and the shell.
380///
381/// To use [`update_app`](CapabilityContext::update_app), [`notify_shell`](CapabilityContext::notify_shell)
382/// or [`request_from_shell`](CapabilityContext::request_from_shell), spawn a task first.
383///
384/// For example (from `crux_time`)
385///
386/// ```rust
387/// # #[derive(Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub struct TimeRequest;
388/// # #[derive(Clone, serde::Deserialize)] pub struct TimeResponse(pub String);
389/// # impl crux_core::capability::Operation for TimeRequest {
390/// # type Output = TimeResponse;
391/// # }
392/// # pub struct Time<Ev> {
393/// # context: crux_core::capability::CapabilityContext<TimeRequest, Ev>,
394/// # }
395/// # impl<Ev> Time<Ev> where Ev: 'static, {
396/// # pub fn new(context: crux_core::capability::CapabilityContext<TimeRequest, Ev>) -> Self {
397/// # Self { context }
398/// # }
399///
400/// pub fn get<F>(&self, callback: F)
401/// where
402/// F: FnOnce(TimeResponse) -> Ev + Send + Sync + 'static,
403/// {
404/// let ctx = self.context.clone();
405/// self.context.spawn(async move {
406/// let response = ctx.request_from_shell(TimeRequest).await;
407///
408/// ctx.update_app(callback(response));
409/// });
410/// }
411/// # }
412/// ```
413///
414// used in docs/internals/runtime.md
415// ANCHOR: capability_context
416pub struct CapabilityContext<Op, Event>
417where
418 Op: Operation,
419{
420 inner: std::sync::Arc<ContextInner<Op, Event>>,
421}
422
423struct ContextInner<Op, Event>
424where
425 Op: Operation,
426{
427 shell_channel: Sender<Request<Op>>,
428 app_channel: Sender<Event>,
429 spawner: executor::Spawner,
430}
431// ANCHOR_END: capability_context
432
433/// Initial version of capability Context which has not yet been specialized to a chosen capability
434pub struct ProtoContext<Eff, Event> {
435 shell_channel: Sender<Eff>,
436 app_channel: Sender<Event>,
437 spawner: executor::Spawner,
438}
439
440impl<Eff, Event> Clone for ProtoContext<Eff, Event> {
441 fn clone(&self) -> Self {
442 Self {
443 shell_channel: self.shell_channel.clone(),
444 app_channel: self.app_channel.clone(),
445 spawner: self.spawner.clone(),
446 }
447 }
448}
449
450// CommandSpawner is a temporary bridge between the channel type used by the Command and the channel type
451// used by the core. Once the old capability support is removed, we should be able to remove this in favour
452// of the Command's ability to be hosted on a pair of channels
453pub(crate) struct CommandSpawner<Effect, Event> {
454 context: ProtoContext<Effect, Event>,
455}
456
457impl<Effect, Event> CommandSpawner<Effect, Event> {
458 pub(crate) fn new(context: ProtoContext<Effect, Event>) -> Self {
459 Self { context }
460 }
461
462 pub(crate) fn spawn(&self, mut command: Command<Effect, Event>)
463 where
464 Command<Effect, Event>: Stream<Item = CommandOutput<Effect, Event>>,
465 Effect: Unpin + Send + 'static,
466 Event: Unpin + Send + 'static,
467 {
468 self.context.spawner.spawn({
469 let context = self.context.clone();
470
471 async move {
472 while let Some(output) = command.next().await {
473 match output {
474 CommandOutput::Effect(effect) => context.shell_channel.send(effect),
475 CommandOutput::Event(event) => context.app_channel.send(event),
476 }
477 }
478 }
479 });
480 }
481}
482
483impl<Op, Ev> Clone for CapabilityContext<Op, Ev>
484where
485 Op: Operation,
486{
487 fn clone(&self) -> Self {
488 Self {
489 inner: Arc::clone(&self.inner),
490 }
491 }
492}
493
494impl<Eff, Ev> ProtoContext<Eff, Ev>
495where
496 Ev: 'static,
497 Eff: 'static,
498{
499 pub(crate) fn new(
500 shell_channel: Sender<Eff>,
501 app_channel: Sender<Ev>,
502 spawner: executor::Spawner,
503 ) -> Self {
504 Self {
505 shell_channel,
506 app_channel,
507 spawner,
508 }
509 }
510
511 /// Specialize the `CapabilityContext` to a specific capability, wrapping its operations into
512 /// an Effect `Ef`. The `func` argument will typically be an Effect variant constructor, but
513 /// can be any function taking the capability's operation type and returning
514 /// the effect type.
515 ///
516 /// This will likely only be called from the implementation of [`WithContext`]
517 /// for the app's `Capabilities` type. You should not need to call this function directly.
518 pub fn specialize<Op, F>(&self, func: F) -> CapabilityContext<Op, Ev>
519 where
520 F: Fn(Request<Op>) -> Eff + Sync + Send + Copy + 'static,
521 Op: Operation,
522 {
523 CapabilityContext::new(
524 self.shell_channel.map_input(func),
525 self.app_channel.clone(),
526 self.spawner.clone(),
527 )
528 }
529}
530
531impl<Op, Ev> CapabilityContext<Op, Ev>
532where
533 Op: Operation,
534 Ev: 'static,
535{
536 pub(crate) fn new(
537 shell_channel: Sender<Request<Op>>,
538 app_channel: Sender<Ev>,
539 spawner: executor::Spawner,
540 ) -> Self {
541 let inner = Arc::new(ContextInner {
542 shell_channel,
543 app_channel,
544 spawner,
545 });
546
547 CapabilityContext { inner }
548 }
549
550 /// Spawn a task to do the asynchronous work. Within the task, async code
551 /// can be used to interact with the Shell and the App.
552 pub fn spawn(&self, f: impl Future<Output = ()> + 'static + Send) {
553 self.inner.spawner.spawn(f);
554 }
555
556 /// Send an effect request to the shell in a fire and forget fashion. The
557 /// provided `operation` does not expect anything to be returned back.
558 #[allow(clippy::unused_async)]
559 pub async fn notify_shell(&self, operation: Op) {
560 // This function might look like it doesn't need to be async but
561 // it's important that it is. It forces all capabilities to
562 // spawn onto the executor which keeps the ordering of effects
563 // consistent with their function calls.
564 self.inner
565 .shell_channel
566 .send(Request::resolves_never(operation));
567 }
568
569 /// Send an event to the app. The event will be processed on the next
570 /// run of the update loop. You can call `update_app` several times,
571 /// the events will be queued up and processed sequentially after your
572 /// async task either `await`s or finishes.
573 pub fn update_app(&self, event: Ev) {
574 self.inner.app_channel.send(event);
575 }
576
577 /// Transform the `CapabilityContext` into one which uses the provided function to
578 /// map each event dispatched with `update_app` to a different event type.
579 ///
580 /// This is useful when composing apps from modules to wrap a submodule's
581 /// event type with a specific variant of the parent module's event, so it can
582 /// be forwarded to the submodule when received.
583 ///
584 /// In a typical case you would implement `From` on the submodule's `Capabilities` type
585 ///
586 /// ```rust
587 /// # use crux_core::{Capability, Command};
588 /// # #[derive(Default)]
589 /// # struct App;
590 /// # pub enum Event {
591 /// # Submodule(child::Event),
592 /// # }
593 /// # #[derive(crux_core::macros::Effect)]
594 /// # pub struct Capabilities {
595 /// # some_capability: crux_time::Time<Event>,
596 /// # render: crux_core::render::Render<Event>,
597 /// # }
598 /// # impl crux_core::App for App {
599 /// # type Event = Event;
600 /// # type Model = ();
601 /// # type ViewModel = ();
602 /// # type Capabilities = Capabilities;
603 /// # type Effect = Effect;
604 /// # fn update(
605 /// # &self,
606 /// # _event: Self::Event,
607 /// # _model: &mut Self::Model,
608 /// # _caps: &Self::Capabilities,
609 /// # ) -> Command<Effect, Event> {
610 /// # unimplemented!()
611 /// # }
612 /// # fn view(&self, _model: &Self::Model) -> Self::ViewModel {
613 /// # unimplemented!()
614 /// # }
615 /// # }
616 ///impl From<&Capabilities> for child::Capabilities {
617 /// fn from(incoming: &Capabilities) -> Self {
618 /// child::Capabilities {
619 /// some_capability: incoming.some_capability.map_event(Event::Submodule),
620 /// render: incoming.render.map_event(Event::Submodule),
621 /// }
622 /// }
623 ///}
624 /// # mod child {
625 /// # #[derive(Default)]
626 /// # struct App;
627 /// # pub struct Event;
628 /// # #[derive(crux_core::macros::Effect)]
629 /// # pub struct Capabilities {
630 /// # pub some_capability: crux_time::Time<Event>,
631 /// # pub render: crux_core::render::Render<Event>,
632 /// # }
633 /// # impl crux_core::App for App {
634 /// # type Event = Event;
635 /// # type Model = ();
636 /// # type ViewModel = ();
637 /// # type Capabilities = Capabilities;
638 /// # type Effect = Effect;
639 /// # fn update(
640 /// # &self,
641 /// # _event: Self::Event,
642 /// # _model: &mut Self::Model,
643 /// # _caps: &Self::Capabilities,
644 /// # ) -> crux_core::Command<Effect, Event> {
645 /// # unimplemented!()
646 /// # }
647 /// # fn view(&self, _model: &Self::Model) -> Self::ViewModel {
648 /// # unimplemented!()
649 /// # }
650 /// # }
651 /// # }
652 /// ```
653 ///
654 /// in the parent module's `update` function, you can then call `.into()` on the
655 /// capabilities, before passing them down to the submodule.
656 pub fn map_event<NewEv, F>(&self, func: F) -> CapabilityContext<Op, NewEv>
657 where
658 F: Fn(NewEv) -> Ev + Sync + Send + 'static,
659 NewEv: 'static,
660 {
661 CapabilityContext::new(
662 self.inner.shell_channel.clone(),
663 self.inner.app_channel.map_input(func),
664 self.inner.spawner.clone(),
665 )
666 }
667
668 pub(crate) fn send_request(&self, request: Request<Op>) {
669 self.inner.shell_channel.send(request);
670 }
671}
672
673#[cfg(test)]
674mod tests {
675 use serde::{Deserialize, Serialize};
676 use static_assertions::assert_impl_all;
677
678 use super::*;
679
680 #[allow(dead_code)]
681 enum Effect {}
682
683 #[allow(dead_code)]
684 enum Event {}
685
686 #[derive(PartialEq, Clone, Serialize, Deserialize)]
687 struct Op {}
688
689 impl Operation for Op {
690 type Output = ();
691 }
692
693 assert_impl_all!(ProtoContext<Effect, Event>: Send, Sync);
694 assert_impl_all!(CapabilityContext<Op, Event>: Send, Sync);
695}