diff --git a/resource.capnp b/resource.capnp index 8fb6f1d..159fc97 100644 --- a/resource.capnp +++ b/resource.capnp @@ -46,72 +46,87 @@ struct Resource { # in a free-form format. # Similar to the human-meaningful name this description can be translated. - notifiable @4 :Notifiable; + grants @4 :ResourceCaps; + + grant :union { + # If the current session has already been given a grant this field will contain a reference + # to it. Since stronger grants extend weaker grants only one of these needs to be set at any + # given point. + # This is mostly useful for session resumption. + + none @5 :Void; + # No previous grant for this resource exists for the current user + + notify @6 :Notify; + interest @7 :Interest; + claim @8 :Claim; + # The user has a respective grant on the resource + } +} + +interface ResourceCaps { + # Capabilities transfered for a resource. Users will have some or all of these set to non-null + # depending on their permission level. + + getState @0 () -> State; # 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. + setNotify @1 ( callback :Callback ) -> ( notify :Notify ); + # Notify allows clients to be informed about state changes asyncronously. A client can register + # a callback that is called every time state changes happen to the resource in question. + # Notify callbacks are ephermal. If the connection to the server is lost any callbacks from that + # client for any resource are unregistered. + + claim @2 () -> ClaimResponse; + # Request 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; + interest @3 () -> ( interest :Interest ); + # Register an "Interest" on this resource. # 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. + + override @4 () -> ( claim :Claim ); + # Override forces a claim to a resource, even if it is already exhausted. This is + # primarely useful for administrative overrides. } -interface Notifiable { +struct ClaimResponse { + enum Error { + # Error describing why a claim failed. -} + exhausted @0; + # There are no more free Claim slots -interface Interestable { + locked @1; + # The resource was locked -} + precondition @2; + # Some precondition was not met -interface Claimable { - # Having this capability set (i.e. not be a `nullptr`) means the user has at least writeable - # access to a resource + dependencies @3; + # Resource failed to secure dependencies + } - 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; + union { + failed :group { + error @0 :Error; + reason @1 :L10NString; } + success @2 :Claim; } } - struct Map(Key, Value) { # A serialized key-value map represented as a list of (k,v) tuples. @@ -123,7 +138,7 @@ struct Map(Key, Value) { } } -using State = Map(Oid, Value); +using State = Map(OID, 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. @@ -149,76 +164,54 @@ using State = Map(Oid, Value); interface Notify { - # 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. It also allows - # syncronous read access to a resource's output state. - # Notify are ephermal. If the connection to the server is lost all `Notify` from that client are - # unregistered. + # If an user has a notify callback registered it can use this capability to remove it again - readOutput @0 State; - # TODO: There should probably be a more efficient approach for reading state than "read *all* - # state". + remove @0 (); + # Remove any notify callbacks from this user for this resource. - setNotify @1 ( 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. + install @1 ( callback :Callback ); + # Install a notify callback, replacing any existing one. This method is useful when getting this + # interface implicitly via Interest or Claim. +} - delNotify @2 () -> :Bool; - # Unregister a registered callback +interface Callback { + # This callback interface needs to be implemented on the client - 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. - 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. - } + # TODO: There should probably be a more efficient approach here too, something along the + # lines of server-side filtering. } interface Interest extends(Notify) { - # "Interest" right now it tells BFFH that the client wants at least one `Claim` to remain. + # "Interest" right now tells BFFH that the client wants at least one `Claim` to remain. + # However, more generally an Interest allows hooking into state changes and block or modify + # them. - 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. + dropInterest @0 (); + # Remove this interest from a resource. - 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. + lock @1 (); + # Lock a resource, making all future state changes from any user but the current one fail until + # the lock is released. - 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. - - } + unlock @2 (); + # Unlock the resource again, allowing other users to change state again. } -interface Claim extends(Access) { +interface Claim extends(Interest) { # 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 @@ -234,6 +227,9 @@ interface Claim extends(Access) { update @1 State -> UpdateResult; # Update the State of the claimed resource with the given one + dropClaim @2 (); + # Drop this claim + struct UpdateResult { enum Error { # Reason why the update failed @@ -249,12 +245,16 @@ interface Claim extends(Access) { typeError @3; # A field in the update has a known identifier but a bad type for that identifier + + locked @4; + # The state is currently locked and can not be modified by anybody but the user that + # issued the lock. } union { failed :group { error @0 :Error; - field @1 :Text; + field @1 :OID; reason @2 :L10NString; } success @3 :Void; diff --git a/utils.capnp b/utils.capnp index 954bdc1..f906ade 100644 --- a/utils.capnp +++ b/utils.capnp @@ -53,7 +53,7 @@ struct UUID { } struct OID { - bytes @0 :Data + bytes @0 :Data; # The OID, encoded as a sequence of varints. In this encoding the lower 7 bits of each octet # contain data bits while the MSB indicates if the *following* octet is still part of this edge. # It is the same encoding UTF-8 uses. To decode you simply collect octets until you find an