@0x8c2f829df1930cd5; using Rust = import "programming_language/rust.capnp"; $Rust.parentModule("schema"); using CSharp = import "programming_language/csharp.capnp"; $CSharp.namespace("FabAccessAPI.Schema"); using Persistent = import "/capnp/persistent.capnp".Persistent; using Value = import "/capnp/schema.capnp".Value; using General = import "general.capnp"; using User = import "user.capnp".User; using Space = import "space.capnp".Space; struct Node { # A node in the state tree. If it's the root note this struct "contains" the whole tree. # TODO: I'm not happy with this representation. While it's about as generic as we can get it's # unhandly because all clients and servers have to always manually check every leaf of the # state tree, relying on convention instead of static type checking. But I'm not sure how else # to represent the state extensibly in a way that lets us evolve the protocol by stabilizing # extensions. One option could be to use OID or UUID as "tag bits" and "stabilize" them by # defining those as `const` values, but that wouldn't give us proper type checking either. part @0 :Text; # Name of the node, making up a path to this node (e.g. "set/colour/red") union { # Content of a node. A node has either children *or* a value, not both. children @1 :List(Node); # Node is not a leaf node ⇒ it has a list of children value @2 :Value; # Node is a leaf node ⇒ it contains a (typed) Cap'n Proto value. # The type `Value` comes from the Cap'n Proto schema definition file (usually # /usr/include/capnp/schema.capnp) and can be any basic capnp type, including lists and # structs (as :AnyPointer which a client has to cast) } } struct Applied { # Encodes if a specific actor has applied/verified a state change name @0 :Text; # Name of the actor state @1 :State; # State of the state change in the actor enum State { unapplied @0; applied @1; verified @2; } } interface Access { # Allow syncronous read access to a resource's state. You're not given this capability directly # but instead Notify, Interest and Claim all extend it, allowing you to call these methods from # any of those. readState @0 Node; readApplied @1 Applied; # TODO: There should probably be a more efficient approach for reading state than "read *all* # state". } interface Notify extends(Access) { # The Notify interface allows clients to be informed about state changes asyncronously. # It is mainly designed around the `register` function which allows a client to register a # Callback on the client that is called every time state changes happen to the resource in # question. # Notify are ephermal. If the connection to the server is lost all `Notify` are unregistered register @0 ( cb: Callback ); # Register a given callback to be called on every state update. If this client already has a # callback registered for this resource the old callback is replaced. # The two fields `state` and `applied` indicate interest for `state` and `applied`. If they are # unset the respective method on the callback will not be called. unregister @1 (); # Unregister the current callback, if any. interface Callback { # This callback interface is implemented on the client newState @0 Node; # A server will call newState() with the updated set state tree if `state` was set to `true` # in `register`, however unless the last call to newState() didn't complete yet, as to not # overload a client. # TODO: There should probably be a more efficient approach here too, something along the # lines of server-side filtering. # TODO: Add newApplied? } } interface Interestable { interest @0 () -> ( interest: Interest ); } interface Interest extends(Access) { register @0 ( cb: Callback ); unregister @1 (); blocking @2 (); # As an alternative to the `register`/`Callback` system you can also call `blocking` which will # — as the name suggests — block until the last claim was dropped. interface Callback { drop @0 (); # The last claim on the resource this Interest is registered on was dropped, invalidating # the Interest. } } interface Claimable { # Having this capability set (i.e. not be a `nullptr`) means the user has at least writeable # access to a resource and the resource is claimable (n > 0). claim @0 () -> ClaimResponse; # Assert a claim on a resource. } interface Lockable { # Having this capability set means the user has managerial access to a resource. lock @0 () -> ( lock: Claim ); } struct ClaimResponse { enum Error { # Error describing why a claim failed. exhausted @0; # There are no more free Claim slots locked @1; # The resource was locked } union { error @0 :Error; success @1 :Claim; } } interface Claim extends(Access) { # TODO: extend Persistance. Claims and Interests need to be able to survive a connection loss, # which is exactly what `SturdyRef`/Persistance are designed to provide. The Persistance # interface only provides one method, `save`, returning a `SturdyRef`. A SturdyRef is a generic # and generally speaking opaque type that can be restored to a live capability using some sort # of `Restorer` service. # In this case the `Restorer` service could be `Claimable` / `Interestable` providing a # `restore( ref: SturdyRef )` method. update @0 Node; # Update the State of the claimed resource with the given one } struct Resource { name @0 :Text; description @1 :Text; typeid @2 :Text; notify @3 :Notify; interest @4 :Interest; claimable @5 :Claimable; lockable @6 :Lockable; }