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 fn register_types(generator: &mut crate::typegen::TypeGen) -> crate::typegen::Result {
228 generator.register_type::<Self>()?;
229 generator.register_type::<Self::Output>()?;
230 Ok(())
231 }
232}
233
234/// A type that can be used as a capability operation, but which will never be sent to the shell.
235/// This type is useful for capabilities that don't request effects.
236/// For example, you can use this type as the Operation for a
237/// capability that just composes other capabilities.
238///
239/// e.g.
240/// ```rust
241/// # use crux_core::capability::{CapabilityContext, Never};
242/// # use crux_core::macros::Capability;
243/// #[derive(Capability)]
244/// pub struct Compose<E> {
245/// context: CapabilityContext<Never, E>,
246/// }
247/// # impl<E> Compose<E> {
248/// # pub fn new(context: CapabilityContext<Never, E>) -> Self {
249/// # Self { context }
250/// # }
251/// # }
252///
253/// ```
254#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
255pub enum Never {}
256
257/// Implement `Operation` for `Never` to allow using it as a capability operation.
258impl Operation for Never {
259 type Output = ();
260}
261
262/// Implement the `Capability` trait for your capability. This will allow
263/// mapping events when composing apps from submodules.
264///
265/// Note that this implementation can be generated by the `crux_core::macros::Capability` derive macro.
266///
267/// Example:
268///
269/// ```rust
270/// # use crux_core::{Capability, capability::{CapabilityContext, Operation}};
271/// # pub struct Http<Ev> {
272/// # context: CapabilityContext<HttpRequest, Ev>,
273/// # }
274/// # #[derive(Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)] pub struct HttpRequest;
275/// # impl Operation for HttpRequest {
276/// # type Output = ();
277/// # }
278/// # impl<Ev> Http<Ev> where Ev: 'static, {
279/// # pub fn new(context: CapabilityContext<HttpRequest, Ev>) -> Self {
280/// # Self { context }
281/// # }
282/// # }
283/// impl<Ev> Capability<Ev> for Http<Ev> {
284/// type Operation = HttpRequest;
285/// type MappedSelf<MappedEv> = Http<MappedEv>;
286///
287/// fn map_event<F, NewEvent>(&self, f: F) -> Self::MappedSelf<NewEvent>
288/// where
289/// F: Fn(NewEvent) -> Ev + Send + Sync + 'static,
290/// Ev: 'static,
291/// NewEvent: 'static,
292/// {
293/// Http::new(self.context.map_event(f))
294/// }
295/// }
296/// ```
297pub trait Capability<Ev> {
298 type Operation: Operation + DeserializeOwned;
299
300 type MappedSelf<MappedEv>;
301
302 fn map_event<F, NewEv>(&self, f: F) -> Self::MappedSelf<NewEv>
303 where
304 F: Fn(NewEv) -> Ev + Send + Sync + 'static,
305 Ev: 'static,
306 NewEv: 'static + Send;
307}
308
309/// Allows Crux to construct app's set of required capabilities, providing context
310/// they can then use to request effects and dispatch events.
311///
312/// `new_with_context` is called by Crux and should return an instance of the app's `Capabilities` type with
313/// all capabilities constructed with context passed in. Use `Context::specialize` to
314/// create an appropriate context instance with the effect constructor which should
315/// wrap the requested operations.
316///
317/// Note that this implementation can be generated by the derive macro `crux_core::macros::Effect`.
318///
319/// ```rust
320/// # #[derive(Default)]
321/// # struct App;
322/// # pub enum Event {}
323/// # #[allow(dead_code)]
324/// # pub struct Capabilities {
325/// # http: crux_http::Http<Event>,
326/// # render: crux_core::render::Render<Event>,
327/// # }
328/// # pub enum Effect {
329/// # Http(crux_core::Request<<crux_http::Http<Event> as crux_core::capability::Capability<Event>>::Operation>),
330/// # Render(crux_core::Request<<crux_core::render::Render<Event> as crux_core::capability::Capability<Event>>::Operation>),
331/// # }
332/// # #[derive(serde::Serialize)]
333/// # pub enum EffectFfi {
334/// # Http(<crux_http::Http<Event> as crux_core::capability::Capability<Event>>::Operation),
335/// # Render(<crux_core::render::Render<Event> as crux_core::capability::Capability<Event>>::Operation),
336/// # }
337/// # impl crux_core::App for App {
338/// # type Event = Event;
339/// # type Model = ();
340/// # type ViewModel = ();
341/// # type Capabilities = Capabilities;
342/// # type Effect = Effect;
343/// # fn update(&self, _event: Self::Event, _model: &mut Self::Model, _caps: &Self::Capabilities) -> crux_core::Command<Effect, Event> {
344/// # unimplemented!()
345/// # }
346/// # fn view(&self, _model: &Self::Model) -> Self::ViewModel {
347/// # unimplemented!()
348/// # }
349/// # }
350/// # impl crux_core::Effect for Effect {
351/// # type Ffi = EffectFfi;
352/// # fn serialize(self) -> (Self::Ffi, crux_core::bridge::ResolveSerialized) {
353/// # match self {
354/// # Effect::Http(request) => request.serialize(EffectFfi::Http),
355/// # Effect::Render(request) => request.serialize(EffectFfi::Render),
356/// # }
357/// # }
358/// # }
359/// impl crux_core::WithContext<Event, Effect> for Capabilities {
360/// fn new_with_context(
361/// context: crux_core::capability::ProtoContext<Effect, Event>,
362/// ) -> Capabilities {
363/// Capabilities {
364/// http: crux_http::Http::new(context.specialize(Effect::Http)),
365/// render: crux_core::render::Render::new(context.specialize(Effect::Render)),
366/// }
367/// }
368/// }
369/// ```
370pub trait WithContext<Ev, Ef> {
371 fn new_with_context(context: ProtoContext<Ef, Ev>) -> Self;
372}
373
374impl<Event, Effect> WithContext<Event, Effect> for () {
375 fn new_with_context(_context: ProtoContext<Effect, Event>) -> Self {}
376}
377
378/// An interface for capabilities to interact with the app and the shell.
379///
380/// To use [`update_app`](CapabilityContext::update_app), [`notify_shell`](CapabilityContext::notify_shell)
381/// or [`request_from_shell`](CapabilityContext::request_from_shell), spawn a task first.
382///
383/// For example (from `crux_time`)
384///
385/// ```rust
386/// # #[derive(Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub struct TimeRequest;
387/// # #[derive(Clone, serde::Deserialize)] pub struct TimeResponse(pub String);
388/// # impl crux_core::capability::Operation for TimeRequest {
389/// # type Output = TimeResponse;
390/// # }
391/// # pub struct Time<Ev> {
392/// # context: crux_core::capability::CapabilityContext<TimeRequest, Ev>,
393/// # }
394/// # impl<Ev> Time<Ev> where Ev: 'static, {
395/// # pub fn new(context: crux_core::capability::CapabilityContext<TimeRequest, Ev>) -> Self {
396/// # Self { context }
397/// # }
398///
399/// pub fn get<F>(&self, callback: F)
400/// where
401/// F: FnOnce(TimeResponse) -> Ev + Send + Sync + 'static,
402/// {
403/// let ctx = self.context.clone();
404/// self.context.spawn(async move {
405/// let response = ctx.request_from_shell(TimeRequest).await;
406///
407/// ctx.update_app(callback(response));
408/// });
409/// }
410/// # }
411/// ```
412///
413// used in docs/internals/runtime.md
414// ANCHOR: capability_context
415pub struct CapabilityContext<Op, Event>
416where
417 Op: Operation,
418{
419 inner: std::sync::Arc<ContextInner<Op, Event>>,
420}
421
422struct ContextInner<Op, Event>
423where
424 Op: Operation,
425{
426 shell_channel: Sender<Request<Op>>,
427 app_channel: Sender<Event>,
428 spawner: executor::Spawner,
429}
430// ANCHOR_END: capability_context
431
432/// Initial version of capability Context which has not yet been specialized to a chosen capability
433pub struct ProtoContext<Eff, Event> {
434 shell_channel: Sender<Eff>,
435 app_channel: Sender<Event>,
436 spawner: executor::Spawner,
437}
438
439impl<Eff, Event> Clone for ProtoContext<Eff, Event> {
440 fn clone(&self) -> Self {
441 Self {
442 shell_channel: self.shell_channel.clone(),
443 app_channel: self.app_channel.clone(),
444 spawner: self.spawner.clone(),
445 }
446 }
447}
448
449// CommandSpawner is a temporary bridge between the channel type used by the Command and the channel type
450// used by the core. Once the old capability support is removed, we should be able to remove this in favour
451// of the Command's ability to be hosted on a pair of channels
452pub(crate) struct CommandSpawner<Effect, Event> {
453 context: ProtoContext<Effect, Event>,
454}
455
456impl<Effect, Event> CommandSpawner<Effect, Event> {
457 pub(crate) fn new(context: ProtoContext<Effect, Event>) -> Self {
458 Self { context }
459 }
460
461 pub(crate) fn spawn(&self, mut command: Command<Effect, Event>)
462 where
463 Command<Effect, Event>: Stream<Item = CommandOutput<Effect, Event>>,
464 Effect: Unpin + Send + 'static,
465 Event: Unpin + Send + 'static,
466 {
467 self.context.spawner.spawn({
468 let context = self.context.clone();
469
470 async move {
471 while let Some(output) = command.next().await {
472 match output {
473 CommandOutput::Effect(effect) => context.shell_channel.send(effect),
474 CommandOutput::Event(event) => context.app_channel.send(event),
475 }
476 }
477 }
478 });
479 }
480}
481
482impl<Op, Ev> Clone for CapabilityContext<Op, Ev>
483where
484 Op: Operation,
485{
486 fn clone(&self) -> Self {
487 Self {
488 inner: Arc::clone(&self.inner),
489 }
490 }
491}
492
493impl<Eff, Ev> ProtoContext<Eff, Ev>
494where
495 Ev: 'static,
496 Eff: 'static,
497{
498 pub(crate) fn new(
499 shell_channel: Sender<Eff>,
500 app_channel: Sender<Ev>,
501 spawner: executor::Spawner,
502 ) -> Self {
503 Self {
504 shell_channel,
505 app_channel,
506 spawner,
507 }
508 }
509
510 /// Specialize the CapabilityContext to a specific capability, wrapping its operations into
511 /// an Effect `Ef`. The `func` argument will typically be an Effect variant constructor, but
512 /// can be any function taking the capability's operation type and returning
513 /// the effect type.
514 ///
515 /// This will likely only be called from the implementation of [`WithContext`]
516 /// for the app's `Capabilities` type. You should not need to call this function directly.
517 pub fn specialize<Op, F>(&self, func: F) -> CapabilityContext<Op, Ev>
518 where
519 F: Fn(Request<Op>) -> Eff + Sync + Send + Copy + 'static,
520 Op: Operation,
521 {
522 CapabilityContext::new(
523 self.shell_channel.map_input(func),
524 self.app_channel.clone(),
525 self.spawner.clone(),
526 )
527 }
528}
529
530impl<Op, Ev> CapabilityContext<Op, Ev>
531where
532 Op: Operation,
533 Ev: 'static,
534{
535 pub(crate) fn new(
536 shell_channel: Sender<Request<Op>>,
537 app_channel: Sender<Ev>,
538 spawner: executor::Spawner,
539 ) -> Self {
540 let inner = Arc::new(ContextInner {
541 shell_channel,
542 app_channel,
543 spawner,
544 });
545
546 CapabilityContext { inner }
547 }
548
549 /// Spawn a task to do the asynchronous work. Within the task, async code
550 /// can be used to interact with the Shell and the App.
551 pub fn spawn(&self, f: impl Future<Output = ()> + 'static + Send) {
552 self.inner.spawner.spawn(f);
553 }
554
555 /// Send an effect request to the shell in a fire and forget fashion. The
556 /// provided `operation` does not expect anything to be returned back.
557 pub async fn notify_shell(&self, operation: Op) {
558 // This function might look like it doesn't need to be async but
559 // it's important that it is. It forces all capabilities to
560 // spawn onto the executor which keeps the ordering of effects
561 // consistent with their function calls.
562 self.inner
563 .shell_channel
564 .send(Request::resolves_never(operation));
565 }
566
567 /// Send an event to the app. The event will be processed on the next
568 /// run of the update loop. You can call `update_app` several times,
569 /// the events will be queued up and processed sequentially after your
570 /// async task either `await`s or finishes.
571 pub fn update_app(&self, event: Ev) {
572 self.inner.app_channel.send(event);
573 }
574
575 /// Transform the CapabilityContext into one which uses the provided function to
576 /// map each event dispatched with `update_app` to a different event type.
577 ///
578 /// This is useful when composing apps from modules to wrap a submodule's
579 /// event type with a specific variant of the parent module's event, so it can
580 /// be forwarded to the submodule when received.
581 ///
582 /// In a typical case you would implement `From` on the submodule's `Capabilities` type
583 ///
584 /// ```rust
585 /// # use crux_core::{Capability, Command};
586 /// # #[derive(Default)]
587 /// # struct App;
588 /// # pub enum Event {
589 /// # Submodule(child::Event),
590 /// # }
591 /// # #[derive(crux_core::macros::Effect)]
592 /// # pub struct Capabilities {
593 /// # some_capability: crux_time::Time<Event>,
594 /// # render: crux_core::render::Render<Event>,
595 /// # }
596 /// # impl crux_core::App for App {
597 /// # type Event = Event;
598 /// # type Model = ();
599 /// # type ViewModel = ();
600 /// # type Capabilities = Capabilities;
601 /// # type Effect = Effect;
602 /// # fn update(
603 /// # &self,
604 /// # _event: Self::Event,
605 /// # _model: &mut Self::Model,
606 /// # _caps: &Self::Capabilities,
607 /// # ) -> Command<Effect, Event> {
608 /// # unimplemented!()
609 /// # }
610 /// # fn view(&self, _model: &Self::Model) -> Self::ViewModel {
611 /// # unimplemented!()
612 /// # }
613 /// # }
614 ///impl From<&Capabilities> for child::Capabilities {
615 /// fn from(incoming: &Capabilities) -> Self {
616 /// child::Capabilities {
617 /// some_capability: incoming.some_capability.map_event(Event::Submodule),
618 /// render: incoming.render.map_event(Event::Submodule),
619 /// }
620 /// }
621 ///}
622 /// # mod child {
623 /// # #[derive(Default)]
624 /// # struct App;
625 /// # pub struct Event;
626 /// # #[derive(crux_core::macros::Effect)]
627 /// # pub struct Capabilities {
628 /// # pub some_capability: crux_time::Time<Event>,
629 /// # pub render: crux_core::render::Render<Event>,
630 /// # }
631 /// # impl crux_core::App for App {
632 /// # type Event = Event;
633 /// # type Model = ();
634 /// # type ViewModel = ();
635 /// # type Capabilities = Capabilities;
636 /// # type Effect = Effect;
637 /// # fn update(
638 /// # &self,
639 /// # _event: Self::Event,
640 /// # _model: &mut Self::Model,
641 /// # _caps: &Self::Capabilities,
642 /// # ) -> crux_core::Command<Effect, Event> {
643 /// # unimplemented!()
644 /// # }
645 /// # fn view(&self, _model: &Self::Model) -> Self::ViewModel {
646 /// # unimplemented!()
647 /// # }
648 /// # }
649 /// # }
650 /// ```
651 ///
652 /// in the parent module's `update` function, you can then call `.into()` on the
653 /// capabilities, before passing them down to the submodule.
654 pub fn map_event<NewEv, F>(&self, func: F) -> CapabilityContext<Op, NewEv>
655 where
656 F: Fn(NewEv) -> Ev + Sync + Send + 'static,
657 NewEv: 'static,
658 {
659 CapabilityContext::new(
660 self.inner.shell_channel.clone(),
661 self.inner.app_channel.map_input(func),
662 self.inner.spawner.clone(),
663 )
664 }
665
666 pub(crate) fn send_request(&self, request: Request<Op>) {
667 self.inner.shell_channel.send(request);
668 }
669}
670
671#[cfg(test)]
672mod tests {
673 use serde::{Deserialize, Serialize};
674 use static_assertions::assert_impl_all;
675
676 use super::*;
677
678 #[allow(dead_code)]
679 enum Effect {}
680
681 #[allow(dead_code)]
682 enum Event {}
683
684 #[derive(PartialEq, Clone, Serialize, Deserialize)]
685 struct Op {}
686
687 impl Operation for Op {
688 type Output = ();
689 }
690
691 assert_impl_all!(ProtoContext<Effect, Event>: Send, Sync);
692 assert_impl_all!(CapabilityContext<Op, Event>: Send, Sync);
693}