@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 User = import "user.capnp".User; using L10NString = import "utils.capnp".L10NString; using UUID = import "utils.capnp".UUID; struct Resource { # BFFH's smallest unit of a physical or abstract "thing". # A resource can be as simple and physical as a table, as complex as a PCB production line or as # abstract as "people with specific know-how are present". uuid @0 :UUID; # An stable, globally unique descriptor for a resource. Two resources with the same UUID are # (almost¹) guaranteed to be the same instance, and the UUID of a resource will survive through # server restarts, renaming, reconfiguration etc. # # [¹]: UUID are 128-bit integer. A collision is *possible*, just *very* unlikely. If you # generate 1 billion UUID every second for the next 100 years you have a chance for a collision # of about 50%. id @1 :Text; # Every resource in BFFH has a human-readable "name" that is locally unique, but not persistent. # That is a resource called "hello" today may be called "bye" tomorrow and a resource called # "hi~~" may not be the same resource as the resource called "hi~~" yesterday. This name is # canonical and thus identifying. There is exactly *one* valid representation of this name at # any given point in time. Thus this name can also not be translated. name @2 :L10NString; # A resource may also have a human-meaningful name that is designed to be shown to users. This # name does not have to be unique or identifiable or canonical, its main use is to be # human-meaningful. For example a "name" could be the translations: # - (en, "Prusa SL1 SLA-Printer") # - (de, "Prusa SL1 SLA-Drucker") # - (es, "Impresora 3D de SLA Prusa SL1") description @3 :L10NString; # A resource may have a description attached to tell an user some more information on a resource # in a free-form format. # Similar to the human-meaningful name this description can be translated. notify @4 :Notify; # Readonly access to the state of a resource. # A resource can have "state". State are values attached to a resource that describe a specific # state that users or administrators want this resource to be in. Usually this state consists of # a number of primitive values encoding for example "turned on" or "turned off". # Users with the required permissions may change this state. claimable @5 :Claimable; # Writeable access to the state of a resource. # Resources are semaphores. They allow writeable access for n ∈ ℕ\{0} clients, depending on the # exact resource in question. In some cases n ≔ 1, and the only write access is exclusive. # "Claims" model this by requiring a client to first assert a claim, thus reserving a semaphore # slot or failing if no more are available, and then using this claim to write to a resources # state. interestable @6 :Interestable; # Sometimes clients are not just interested in the state of a resource but rather want a # resource to stay in a specific state. e.g. somebody working in a makerspace wants the space to # stay open, even though they themselves may not have permission to keep the makerspace open. # "Interest" represents this. Specifically right now it tells BFFH that the client wants at # least one `Claim` to remain. } struct Map(Key, Value) { # A serialized key-value map represented as a list of (k,v) tuples. entries @0 : List(Entry); struct Entry { key @0 :Key; val @1 :Value; } } using State = Map(Text, Value); # Update state provided to a resource via a claim is represented as a Map of human-readable # identifiers to Cap'n Proto Values. These Values can be either primitive types such as Uint8, # Float64 or more complex types such as structs, lists, or enums. # The resulting state of a resource, which is the output of whatever internal logic the resource # implements, is also represented in this form, but the keys and also values may be different. # # Later on very common cases (use, register, return, etc.) can get shortcut functions in the Claim # interface that pre-emptively check permissions and ability (so you get the respective cap iff the # resource supports that update and if you're allowed to do that) but these functions only serve to # make the update more efficient than calling `update` with the string identifier and dynamic typed # value but do the exact same serverside as an `update` call would. This way we can make future # versions of the API more efficient and easier to use while not breaking compatibility with old # clients. # # TODO: This has the potential problem that a newer client can not distinguish between a server # using an old version of the API and a client simply not being allowed to call a specific shortcut # method because in both cases that cap will be a nullptr. Could be solved by making `Claim` a # struct and indicating which shortcut methods it knows of. # Not sure if this is a big problem, we optimize for old clients and up-to-date servers. # # TODO: We should provide a number of sensible implementations for common complex `Value` types such # as "colour", "temperature", etc. and define identifiers for common values. interface Access { # Allow syncronous read access to a resource's output state. You're not given this capability # directly but instead Notify, Interest and Claim all extend it, allowing you to call these # methods on any of those. readOutput @0 State; # 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 # 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` from that client 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. unregister @1 (); # Unregister this callback interface Callback { # This callback interface needs to be implemented on the client newState @0 State; # A server will call newState() with the updated output state. However a server will only # allow one in-flight call, so as long as the previous call to newState() hasn't completed # the server will drop intermediary updates as to not overload a client. # Specifically, example timeline: # 1. Update A # 2. Server calls newState(A) # 3. Update B # 4. Update C # 5. Call to newState(A) completes # 6. Server calls newState(C) # So Update B was never sent to the client but the client will eventually always end up with # the latest state. # TODO: There should probably be a more efficient approach here too, something along the # lines of server-side filtering. } } interface Interestable { # "Interest" right now it tells BFFH that the client wants at least one `Claim` to remain. register @0 ( cb: Callback ) -> ( handle :Handle ); # Register a callback that BFFH will use to send notifications back to the client asyncronously. # This creates an "Interest" on this resource. Setting the callback to a `nullptr` will still # register an interest but the server will not be able to inform a client about an impeding # claim drop. blocking @1 (); # As an alternative to the `register`/`Callback` system you can also call `blocking` which will # — as the name suggests — block until the last claim is being dropped. This will register an # ephermal Interest that can not survive a disconnect. interface Callback { drop @0 (); # The last claim on the resource this Interest is registered is being dropped, invalidating # the Interest. } interface Handle { # A Handle back to the server side Interest registered. Destroying this capability will also # inform the server and remove the Interest again. # TODO: `extends (Persistance)` so that clients can `save` this capability and thus make the # Interest survive disconnects. } } 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. 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 precondition @2; # Some precondition was not met dependencies @3; # Resource failed to secure dependencies } union { failed :group { error @0 :Error; reason @1 :L10NString; } success @2 :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. readInput @0 () -> State; # Get the current *input* state. This is not the output state that `Notify` or Actors get access # to but instead the currently stored input state of a resource. update @1 State -> UpdateResult; # Update the State of the claimed resource with the given one struct UpdateResult { enum Error { # Reason why the update failed denied @0; # Update was denied beause user is missing an required permission precondition @1; # Some other precondition failed, e.g. because a required field is not set invalid @2; # The update is invalid, e.g. because an unknown field was set. typeError @3; # A field in the update has a known identifier but a bad type for that identifier } union { failed :group { error @0 :Error; field @1 :Text; reason @2 :L10NString; } success @3 :Void; } } }