capnproto / go-capnp

Cap'n Proto library and code generator for Go
https://capnproto.org
Other
1.22k stars 110 forks source link

Any better documentation? #297

Closed hspaay closed 2 years ago

hspaay commented 2 years ago

Hi, I'm trying out go-capnproto2 using a simple service that is currently working using protobuf-v3. One of the reasons protobuf doesn't work for me is the lack of constants and the requirement of using a different port for every single service. UDS could work, but the memmap and pipes communication in go-capnproto is attractive.

Following the example on the wiki here: https://github.com/capnproto/go-capnproto2/wiki/Getting-Started#rpc-serving, I get stuck on writing the server adapter. Up to this point my own code works including compilation of the .capnp file, importing it in my code, writing an adapter interface. Using the adapter pattern makes a lot of sense.

Implementing the adapter itself however makes no sense at all. I've looked for documentation on how to implement a service but other than the example that has little explanation there isn't much. I tried reading the generated code but am a bit overwhelmed where the capnp file of 30 lines explodes to 890 lines of go code.

Specifically the following example bits are confusing:

  1. Section RPC Serving:
    func serveLogger(ctx context.Context, rwc io.ReadWriteCloser) error {
    // Create a new locally implemented logger.
    main := pkg.Logger_ServerToClient(&loggerAdapterServer{
        internalLogger: &internalLogger{logger: log.New(os.Stdout, "", log.LstdFlags)},
    }, &server.Policy{MaxConcurrentCalls: 250})
    ...

The function 'pkg.Logger_ServerToClient' seems to come out of nowhere. Why is it here? It seems to be related to the client but it isn't clear how and why. Any documentation on this apparently essential function?

  1. Implementing the adapter code

    func (las *loggerAdapterServer) Debug(_ context.Context, call pkg.Logger_debug) error {
    msgPayload, err := call.Args().Msg()
    if err != nil {
        return err
    }
    
    las.internalLogger.Debug(msgPayload)
    return nil
    }

    While it is obvious that this is the adapter implementation of the Debug logging function, it is a mystery where this API is coming from and how to use it. What is the 'call' parameter? what does it do. Any documentation on how to use it?

Example code is great and important but it doesn't replace proper documentation that describes what the steps are to implement a server with links to API documentation. Something that does not require reverse engineering the generated code? I can probably guess some stuff and eventually get things working but it feels I'm missing important documentation.

Any pointers in the right direction would be appreciated.

zenhack commented 2 years ago

Yeah, we're aware the documentation isn't great, and shoring it up is something I want to do before we finally tag 3.0. I'll try to dump a few key ideas here to get you moving; maybe when I've got the time I can cannibalize some of this post to make something more official:


Re: ServerToClient:

A "server" in capnp terminology is an object which handles incoming method calls, while a "client" is an object used to invoke methods on some server, which it points to.

These don't necessarily line up with with any notion of network server & client (in fact, they usually don't): you can have many clients and servers floating around on both sides of a network connection, the clients can point at servers on the other host, or at servers existing in the same process.

Only clients can be passed around via the protocol, so to actually offer your bootstrap capability to a network peer, you first have to get a client for it. This is where the *_ServerToClient functions come in; the code generator emits one of these for each interface type in your schema. It takes something that satisfies the server interface for that type, spawns a goroutine to handle incoming calls for that object (one at a time), and returns a client, which you can set as your bootstrap interface, or pass to a method call, or return from one or...

Minor thing to be aware of: the server.Policy type has been removed on main; the example code should be updated to reflect that (I'm going to just do that now before I forget...)


Re: the call parameter, It offers three things:

It's mostly its own type because un-bundling these things would result in huge method signatures. The v2 API went even further in that the context was stored there as well, but there's such a strong expectation of that being the first argument that we figured the extra verbosity was worth it there...


Hopefully this is helpful, sorry for the current state of affairs, and thanks for the feedback.

hspaay commented 2 years ago

@zenhack Thank you kindly for the explanation and the fast response. This helps a lot. I'll give it another try.

hspaay commented 2 years ago

Can't get my code to compile as 'main' doesn't have a member called Client.

// Listen for calls, using the logger as the bootstrap interface.
    conn := rpc.NewConn(rpc.NewStreamTransport(rwc), &rpc.Options{
        BootstrapClient: main.Client,
    })

I noticed that BootstrapClient needs to be of type capnp.Client, which is puzzling as 'main' is an instance of this type. Is there a better name than 'main' to convey its purpose? To get it to compile this change was needed:

   import capnp "capnproto.org/go/capnp/v3"
  ...
   BootstrapClient: capnp.Client(main),

The import is not there in the example, but it is in the generated code. So maybe this is not the right approach. ... moving on...

zenhack commented 2 years ago

Ah, yeah, that's also bitrot -- we recently changed the generated client types from being defined as type Foo struct { Client capnp.Client } to type Foo capnp.Client, and I guess forgot to update the wiki.

hspaay commented 2 years ago

@zenhack almost there, but for one more problem...
In the example, when creating the client, the field 'Client' doesn't exist.

    // now we generate the "client"
    // it's just called "logger" to make it a bit easier to understand
    logger := pkg.Logger{Client: clientConn.Bootstrap(ctx)}

This produces a compiler error.

If I remove the field, then the client call fails with 'capnp: call on null client'.

The Logger (of type capnp.Client) does have an internal '*client' field but that is not accessible.

Just a suggestion, maybe try to get the example code to run as it exposes quite a few issues... just say'in ;-)

zenhack commented 2 years ago

Quoting Henk (2022-08-29 14:54:28)

Almost there, but for one more problem shows In the example, when creating the client, the field 'Client' doesn't exist. // now we generate the "client" // it's just called "logger" to make it a bit easier to understand logger := pkg.Logger{Client: clientConn.Bootstrap(ctx)}

This produces a compiler error.

Ah, that'd be due to the same API change I mentioned. So you want pkg.Logger(clientConn.Bootstrap(ctx)).

hspaay commented 2 years ago

Thanks @zenhack. One more issue cropped up with this:

The call to resp.Struct() returns this error in my own code:

capnp/svc/CertSvc.capnp:CertServiceCap.createClientCert: vat does not expose a public/bootstrap interface

Code snippet:

resp, release := certClient.CreateClientCert(ctx,
        func(params svc2.CertServiceCap_createClientCert_Params) error {
            fmt.Println("CertServiceCap_createClientCert_Params")
            err = params.SetClientID(clientID)
            err = params.SetPubKeyPEM(pubKeyPEM)
            return err
        })
    defer release()
    result, err := resp.Struct()

The server side does have the bootstrap client registered:

    rwc, _ := lis.Accept()
    transport := rpc.NewStreamTransport(rwc)
    conn := rpc.NewConn(transport, &rpc.Options{
        BootstrapClient: capnp.Client(main),
    })
    defer conn.Close()

I tried stepping through the code but got lost unfortunately. Any thoughts where to look?

zenhack commented 2 years ago

Hunch: try changing to capnp.Client(main.AddRef()); I'm hazarding a guess that what's happening is that the client gets shut down after the first connection closes, so future connections are using a dead client as their bootstrap interface.

The refcounting model is something we also know we need to document better, but there's this wiki page at least: https://github.com/capnproto/go-capnproto2/wiki/Ref-Counting

hspaay commented 2 years ago

@zenhack your hunch foo is strong! That did the trick. You were also right about the connection closing. After use AddRef() when the client call has completed, the connection still closes however. Should the server run in a loop, like this?:

for {
    rwc, _ := lis.Accept()
    transport := rpc.NewStreamTransport(rwc)
    conn := rpc.NewConn(transport, &rpc.Options{
        BootstrapClient: capnp.Client(main),
    })
    defer conn.Close()
    // Wait for connection to abort.
    select {
    case <-ctx.Done():
        return conn.Close()
    }
}

I'm wondering whether creating new transports for each request is the intended way or not? Thanks again,

lthibault commented 2 years ago

@hspaay I've been quietly monitoring this thread, and it's prompted me to revise our our Getting Started guide. Your feedback would be most appreciated! https://github.com/capnproto/go-capnproto2/wiki/Getting-Started

@zenhack your hunch foo is strong! That did the trick.

Ah yes, Ian's capnp hunches are second to none 😄

Should the server run in a loop, like this?:

That seems sensible. I would recommend forking a goroutine to handle the select block, so that you don't block the main loop, but other than that 👍.

I'm wondering whether creating new transports for each request is the intended way or not?

Definitely not, unless those requests are one-off and short-lived (e.g. like an HTTP GET request). Bear in mind that capnp-rpc has a very strong object-oriented model. To wit: you aren't just making RPC calls, here. You are (1) exporting an object over the network and (2) making method calls against that object. As such, the typical pattern is to create a connection for an initial object that you want to export (called the "bootstrap interface"), and for the client to hold it for an indeterminate period of time. Methods on that object can return other objects, which all use the same underlying connection.

I would strongly encourage you to read this excellent article on object capabilities, to understand what this achieves. I think it'll help you think creatively with Cap'n Proto. 🙂

(P.S.: welcome to the community!)

zenhack commented 2 years ago

Quoting Henk (2022-08-29 19:37:58)

Should the server run in a loop, like this?: for { rwc, _ := lis.Accept() transport := rpc.NewStreamTransport(rwc) conn := rpc.NewConn(transport, &rpc.Options{ BootstrapClient: capnp.Client(main), }) defer conn.Close() // Wait for connection to abort. select { case <-ctx.Done(): return conn.Close() } }

I would probably spawn a goroutine after the Accept(), so you can handle more than one connection at a time. But otherwise this looks right.

I'm wondering whether creating new transports for each request is the intended way or not?

You should have a new one for each connection; I'm not sure what you mean by request. Do you mean a method call? If so, no, you'd normally just make more calls on the same client once you've got it.

Note also that @lthibault just rewrote the rpc section of the Getting Started wiki page; wouldn't mind your feedback on that.

hspaay commented 2 years ago

@zenhack @lthibault thank you again for the quick responses.

I would strongly encourage you to read this excellent article on object capabilities, to understand what this achieves. I think it'll help you think creatively with Cap'n Proto. slightly_smiling_face

Wow that is quite the article. Brilliant! Still reading but so-far it scratches an itch that has been growing larger for years. The security aspect is very interesting.

@hspaay I've been quietly monitoring this thread, and it's prompted me to revise our our Getting Started guide. Your feedback would be most appreciated! https://github.com/capnproto/go-capnproto2/wiki/Getting-Started

I'll take a look once I've completed the article :)

To wit: you aren't just making RPC calls, here. You are (1) exporting an object over the network and (2) making method calls against that object.

I originally thought that the request and response operate using a proxy on the object. Sounds like there is a bit more to it.

I would probably spawn a goroutine after the Accept(), so you can handle more than one connection at a time.

Yep that seems to work.

I'm putting an echo service example together that works with gRPC and with capnpro. Maybe it is useful as another example, plus I'm curious about a performance comparison. Hopefully I get to a level of understanding that would allow me to assist with some documentation and examples.

lthibault commented 2 years ago

I originally thought that the request and response operate using a proxy on the object. Sounds like there is a bit more to it.

That's effectively it. I'm using the phrase "export an object" in the conceptual sense, to emphasize the fact that Cap'n Proto wants you to think in terms of networked objects.

Maybe it is useful as another example, plus I'm curious about a performance comparison.

As are we! Looking forward to it!

hspaay commented 2 years ago

Still reading that document, but also had some fun with a test echo service comparing gRPC and go-capnproto2 (v3):

This test compares various payloads and unix domain sockets vs TCP sockets. Performance is fairly evenly matched. Multiple runs provide a 10% variability:

The echo test repo can be found here: https://github.com/hiveot/echorpc (disclaimer: the code is still very crude. It needs better commenting and probably a bunch of bug fixes)

UPDATE 2022-08-31: Use plain 'echo' as 'upper' slows the performance measurement for documents 10K and up. Now a difference between gRPC and Capnproto starts to show for larger documents.

$ go run pkg/main.go

--- test with Hello World payload ---
10000 calls using gRPC  on unix:///tmp/echoservice-grpc.socket: 950 millisec
10000 calls using Capnp on /tmp/echoservice-capnp.socket: 950 millisec
10000 calls using gRPC  on :8991: 1150 millisec
10000 calls using Capnp on :8992: 1430 millisec
--- test with 1K payload ---
10000 calls using gRPC  on unix:///tmp/echoservice-grpc.socket: 870 millisec
10000 calls using Capnp on /tmp/echoservice-capnp.socket: 1140 millisec
10000 calls using gRPC  on :8991: 1260 millisec
10000 calls using Capnp on :8992: 1670 millisec
--- test with 10K payload ---
10000 calls using gRPC  on unix:///tmp/echoservice-grpc.socket: 1410 millisec
10000 calls using Capnp on /tmp/echoservice-capnp.socket: 1310 millisec
10000 calls using gRPC  on :8991: 1170 millisec
10000 calls using Capnp on :8992: 1690 millisec
--- test with 100K payload ---
10000 calls using gRPC  on unix:///tmp/echoservice-grpc.socket: 4670 millisec
10000 calls using Capnp on /tmp/echoservice-capnp.socket: 3270 millisec
10000 calls using gRPC  on :8991: 5190 millisec
10000 calls using Capnp on :8992: 3850 millisec
lthibault commented 2 years ago

@hspaay Neat! I just had a look at the code and one thing you might try is to call call.Ack() inside of the server method. This will cause the call to be handled in a separate goroutine, which might improve performance. I doubt it'll be the case for such a trivial RPC call as Echo, though.

Also, Ian will be doing some performance tuning of the RPC system once #294 is finished, so it'll be interesting to run these microbenchmarks again afterwards.

hspaay commented 2 years ago

@lthibault FYI Just tried the Ack() and there is little difference as you expected. If anything it slowed things down a bit on smaller payloads. Anyways, I'm veering off the original topic, which is documentation. Maybe a separate thread on performance would be useful. I'll continue the reading (the article is a slow read or maybe I'm developing a case of ADHD ;)) and will get you feedback on the example page.

UPDATE: Apparently 'upper' does start to slow things down significantly for 10K and 100K documents so I changed the test to just do echo. This shows a clearer difference between gRPC and Capnproto for documents 10K and 100K.

hspaay commented 2 years ago

@lthibault Some feedback on the first pass of the getting started guide:

Overall, it has some nice improvements and explains some things better. A nice touch is that it shows what to do in case of a compile error 'no such plugin'.

There does seem to be two trains of thought mixed together. One is to show how to use capnp as an RPC. The other to show how to use it for capabilities. Just a thought, but may best to split those two concepts in two examples. The RPC is a stepping stone towards showing capabilities to access a resource like a book store. I don't think you can get away with having to explain capabilities a bit further for golang developers, as the capnp document does a limited job of this.

compiling

To compile this schema into Go code, run the following command: capnp compile -I$GOPATH/src/capnproto.org/go/capnp/std -ogo foo/books.capnp This generates the file foo/books.capnp.go. Note that the path matches the one from the import annotation.

This last line wasn't immediately clear to me as it suggests that the destination folder is determined by the import statement. That isn't the case as it goes into the same folder as the capnp IDL file.

For those that prefer to separate the capnp file from the generated code, the next question is how to change the destination folder? Eg: add the destination folder to the -ogo: option, like this:

  capnp compile -I$GOPATH/src/capnproto.org/go/capnp/std -ogo:/path/to/destinationFolder foo/books.capnp

Also worth mentioning the catch that it repeats the path to the capnp file in the destination folder, eg /path/to/destinationFolder/foo/books.capnp.go. To compile straight into the destinationFolder add the option --src-prefix=foo. This is equivalent to grpc's "paths=source_relative" option.

Marshaling and Unmarshaling Data

This section confused the heck out of me as I thought this was necessary to send or receive a message. Apparently it isn't needed so it threw me off. Some questions that come to mind:

  1. When is this useful? Do I normally have to do this?
  2. What is a "root struct" and why do I care?
  3. When do I need to read from a byte-stream?

Please consider that I'm reading this from the perspective of a developer that wants to get going with the example program. Everything that isn't needed is in the way. I'm sure its useful but maybe in a next chapter or in a FAQ.

Remote calls using interfaces

The intro is good, links to more info and focus on the example. Two notes on those links (please keep them, just an FYI):

  1. The RPC protocol page suggests that performance is much better. 'the results are returned instantly before the server receives the request. This is misleading and makes no sense. What is returned is a proxy to retrieve the result. In most cases you need the result before the next call so the benefit of avoiding a round trip when passing the result to the next call (without processing) is only a special case. I find therefore that it distracts from the real use-case for Capnp as RPC protocol. (which is what exactly? - provide the building blocks for capabilities based infrastructure? - honestly it is not clear)
  2. I wish there was an easier to read introduction to Object Capabilities (second link). I found the description rather wordy and still don't fully understand why security it is better than ACLs. Intuitively it seems right but the article doesn't provide any proof. Someone/something still needs to issue capabilities so if that is compromised aren't you in the same boat as with ACL's? The point that was very clear though is that: applications you run have all the permissions that you have, whether it is related to the application or not, and that is horrible for security. The article leaves a cognitive gap to implementation of managing capabilities to address this, that hinders understanding. Developers will want to have an idea how to solve this problem in their implementations and this is missing. Maybe a good topic for a separate discussion, or another chapter in a tutorial.

Schema

  1. Any reason not to continue with the book theme? Assuming we're building an example application. Not a big deal, just wondering. The book example can show managing a resource (books), and provides an RPC to that resource. The arith example focuses on the RPC only. Or maybe good as two examples. First the most simple form of RPC, second for managing capabilities to use a resource... food for thought.
  2. "Capnp RPC operates on interface types in your schema" ... what does this mean? Is it possible to reword this to help the reader understand what 'operates' does and why as a developer I should care. This statement is important for go developers as an interface in cpnp translates to an interface for the server as described in the next paragraph.

Server Implementation.

The explanation of the compiler generated types is very useful. Maybe elaborate a bit further to explain the purpose of each type.

The confusing bit to is that the example seems to be disconnected from it (it isn't but that isn't obvious).

In a nutshell, while showing 'do this' to make it work is important, the reader's understanding would improve further with a short explanation as to what is idiomatic and what it tries to achieve as mentioned above.

Making RPC calls

I found this example only serves a single client connection, then quits. Maybe useful to show the loop to serve multiple client connections, otherwise it ain't a real server ya'know :)

The comments explain what the code is doing. The questions that did come to mind:

  1. What is this bootstrap thing and why is it important.
  2. Why does a.Multiply take a function that sets the parameters instead of simply setting the parameters directly. This seems a bit of a roundabout way of doing things that is another cognitive step. (for good reason I'm sure).
  3. What does release do?
  4. "You can do other things while the RPC call is in-flight...' Now this is a nice teaser! This begs for another example to demonstrate the power of capnp. Btw, might want to explain what f.Struct() is doing to take the magic out of it.

Thank you kindly for working on an elaborate getting started. I think it is a key piece to get people unfamiliar with capabilities (like myself) started with it. Hopefully this feedback helps.

lthibault commented 2 years ago

@hspaay Firstly, thank you so much for this detailed feedback. This is incredibly helpful!

There does seem to be two trains of thought mixed together. One is to show how to use capnp as an RPC. The other to show how to use it for capabilities.

I'm having trouble seeing where the two could be more cleanly separated. Do you have an example of what RPC sans capabilities might look like?

[...] I don't think you can get away with having to explain capabilities a bit further for golang developers, as the capnp document does a limited job of this.

Just to clarify: are you saying that we need to add a section about capabilities in addition to the existing tutorial, or are you also saying that the current demo is confusing because it conflates RPC with capabilities?

This last line wasn't immediately clear to me as it suggests that the destination folder is determined by the import statement. That isn't the case as it goes into the same folder as the capnp IDL file.

Fixed. ✅

For those that prefer to separate the capnp file from the generated code, the next question is how to change the destination folder?

There's always some tension between between being clear and complete. I've found that people are rapidly overwhelmed by the build process, so I'm of the opinion that we should not address this here. Besides, it's documented in the capnp command itself. @zenhack Agree?

As a compromise, I've added a small callout to the tutorial that tells readers where they can read about compilation options.

This section confused the heck out of me as I thought this was necessary to send or receive a message. Apparently it isn't needed so it threw me off.

Also very useful! I did not expect this section to cause such confusion, so thanks for bringing it to light.


I've unfortunately run out of time for today, but will go over the rest of your feedback carefully tomorrow.

In the meantime, it's become apparent that some additional explanation of concepts will be needed, which means we need to split the wiki page into several tutorials. I've done so, and linked them all together in a (hopefully) intuitive way: https://github.com/capnproto/go-capnproto2/wiki/Getting-Started

hspaay commented 2 years ago

I'm having trouble seeing where the two could be more cleanly separated. Do you have an example of what RPC sans capabilities might look like?

Hah good question. I currently don't have the knowledge to provide a good answer. The simple RPC call case is clear and described in the arith example. Capabilities add an aspect that I don't fully comprehend but think it could address a key concern, that of security. (My use-case is around IoT). For example, in a book store owner 'bob' can add books and reader 'charlie' can read purchased books. How does this play out in a capabilities based system? In an ACL system there is a authorization step that verifies that the caller has permissions to add books. The capabilities article explains that the ACL mechanism is flawed and claims that capabilities solves this. It isn't clear how this is solved though. Seeing this in an example would be a great demonstration of Capabilities based development. Is out of scope for capnproto?

Just to clarify: are you saying that we need to add a section about capabilities in addition to the existing tutorial, or are you also saying that the current demo is confusing because it conflates RPC with capabilities?

An additional section/chapter about capabilities. The current demo presents an API that is different from conventional RPC. Eg the client supplying a method to set the parameters for example. Purely from an RPC point of view is this really a necessary complication? Normally you'd pass the parameter to the method to invoke. It probably makes a lot of sense from a capabilities point of view but I don't understand it enough to be sure. After reading the articles it hasn't sunk in why this approach is better, other than 'it is how capabilities work'. What is gained doing it this way? This is for me the most important question to help understand this API.

There's always some tension between between being clear and complete Besides, it's documented in the capnp command itself.

Yes, true on both accounts.

Thank you for considering my comments.

lthibault commented 2 years ago

@hspaay Continuing yesterday's discussion:

Marshaling and Unmarshaling Data

I've reworked this quite a bit in an attempt to separate three distinct ideas:

  1. Creating, modifying and generally interacting with the capnp generated types
  2. (Un)marshalling these types to and from []byte
  3. Working with streams of capnp objects

I've moved this section to its own page, under the umbrella title of Working with Cap'n Proto Types. Hopefully this is clearer!

Remote calls using interfaces

Points are duly noted. I think I'm going to address this in two steps:

  1. Add a very brief intro to object capabilities in the intro to RPC.
  2. Add a lengthier tutorial about object capabilities and advanced RPC features in a separate page (placeholder)

Schema

Any reason not to continue with the book theme?

No particular reason. That said, Ian suggested the Arith example might feed nicely into this existing calculator example from the C++ capnp implementation.

This would provide a good setting to demonstrate advanced features such as pipelining, along with examples of capability-based access control.

"Capnp RPC operates on interface types in your schema" ... what does this mean?

Agreed this could be clearer. I spent a disproportionate amount of time on that sentence, despite the idea being very simple: RPC in Cap'n Proto is performed by method calls on special objects. You define such an object in the IDL using the interface keyword.

Do you have any thoughts on wording?

The explanation of the compiler generated types is very useful. Maybe elaborate a bit further to explain the purpose of each type.

👍 Will do. Your specific points are also noted (and extremely helpful).

I found this example only serves a single client connection, then quits. Maybe useful to show the loop to serve multiple client connections, otherwise it ain't a real server ya'know :)

Fair point! I'll add a short, final section in which we write the server loop. Explicit is better than implicit, after all 🙂

Your specific questions are noted, and I'll address those in the docs shortly.

Thank you for considering my comments.

And thank you once again for your thoughtful feedback! Writing good documentation is frankly much harder than writing a correct implementation, so your perspective as a newcomer is most precious.

By the way, we have a dedicated Matrix channel that you are welcome to join, if chatting about this stuff in real-time would be helpful. (The above link will direct you to a Web client.)

hspaay commented 2 years ago

I've reworked this quite a bit in an attempt to separate three distinct ideas: I've moved this section to its own page, under the umbrella title of Working with Cap'n Proto Types. Hopefully this is clearer!

Yes this approach works very well and it has an excellent description of what is going on. Well done!

RPC operates on interface types in your schema" ... what does this mean?

Agreed this could be clearer. I spent a disproportionate amount of time on that sentence,...

LOL, indeed, the simplest concepts are sometimes the hardest to explain... Let me give it a try, based on my current understanding ... (which is still rather limited)

"In the Capnp IDL file you define a schema using 'struct's for objects and interfaces for RPC methods. The go-capnp compiler generates a corresponding golang struct type with getter and setter methods for each of the struct members. The RPC interface is used to generate a golang interface for the server implementation, and a client types to invoke the server."

Maybe a bit too wordy though.

I see you're working furiously to add and improve various sections of the documentation. Looking much better already.

One last question, is the v3 API already written in stone? I wonder if there is an opportunity to simplify its usage, or have a higher level API for the main use-cases. I'll get in touch on Matrix. Thanks!

lthibault commented 2 years ago

I see you're working furiously to add and improve various sections of the documentation. Looking much better already.

Good to hear! 😃

One last question, is the v3 API already written in stone? I wonder if there is an opportunity to simplify its usage, or have a higher level API for the main use-cases.

You're asking all the right questions 😉 The v3.0 API is not yet set in stone, and we have a couple of open issues (Exhibit A) regarding opportunities to improve the RPC API.

I'll get in touch on Matrix. Thanks!

See you soon! 👋

lthibault commented 2 years ago

Docs are better than they were before, so closing 😃