From c10dc43f773362a6c923e5a8fa4dc1f788225d70 Mon Sep 17 00:00:00 2001 From: Nadja Reitzenstein Date: Fri, 26 Nov 2021 02:25:47 +0100 Subject: [PATCH] Clean up structure a bit --- resource.capnp | 218 +++++++++++++++++++++++++------------------------ utils.capnp | 13 +++ 2 files changed, 124 insertions(+), 107 deletions(-) diff --git a/resource.capnp b/resource.capnp index 8b36e79..8fb6f1d 100644 --- a/resource.capnp +++ b/resource.capnp @@ -9,6 +9,7 @@ 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; +using OID = import "utils.capnp".OID; struct Resource { # BFFH's smallest unit of a physical or abstract "thing". @@ -45,7 +46,7 @@ struct Resource { # in a free-form format. # Similar to the human-meaningful name this description can be translated. - notify @4 :Notify; + notifiable @4 :Notifiable; # 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 @@ -68,121 +69,17 @@ struct Resource { # least one `Claim` to remain. } -struct Map(Key, Value) { - # A serialized key-value map represented as a list of (k,v) tuples. +interface Notifiable { - 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). + # access to a resource claim @0 () -> ClaimResponse; # Assert a claim on a resource. @@ -214,6 +111,113 @@ interface Claimable { } } + +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(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. +# 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 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. + + readOutput @0 State; + # TODO: There should probably be a more efficient approach for reading state than "read *all* + # state". + + 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. + + delNotify @2 () -> :Bool; + # Unregister a registered 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 Interest extends(Notify) { + # "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 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 diff --git a/utils.capnp b/utils.capnp index c75bc8c..954bdc1 100644 --- a/utils.capnp +++ b/utils.capnp @@ -52,3 +52,16 @@ struct UUID { # upper 8 bytes of the uuid, containing the MSB. } +struct OID { + 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 + # octet <128 and then concat the data bits of all the octets you've accumulated, including the + # current one. This gives you the value of one node. Continue until you've exhausted the + # available data. + # This is a rather efficient encoding since almost all edges of the OID tree are smaller than + # 128 and thus encode into one byte. + # X.208 does *not* limit the size of nodes! However, a reasonable size limit is 128 bit per + # node, which is the size of the UUID nodes in the `2.25` subtree. +}