WIP IPC draft
This commit is contained in:
parent
4c835a467b
commit
b5dabacff4
|
@ -47,7 +47,10 @@ func (m method) UnderlyingComment() Comment { return m.Comment }
|
|||
func (m method) internalMethod() {}
|
||||
|
||||
// GetterMethod is a method that returns a regular value. These methods must not
|
||||
// do any IO. An example of one would be ID() returning ID.
|
||||
// do any IO. An example of one would be ID() returning ID. For methods that
|
||||
// don't take in any parameters, The return values must be constant throughout
|
||||
// the lifespan of the program. As such, the frontend can assume that the return
|
||||
// values will never change and store them.
|
||||
type GetterMethod struct {
|
||||
method
|
||||
|
||||
|
|
|
@ -0,0 +1,327 @@
|
|||
# rpc
|
||||
|
||||
Here are the plans for cchat's backend Remote Procedure Call (RPC) protocols and
|
||||
implementations.
|
||||
|
||||
# TODO
|
||||
|
||||
Move the majority of this into actual Go code.
|
||||
|
||||
# Specifications
|
||||
|
||||
## Preamble
|
||||
|
||||
This reference documentation will use terms such as "GetterMethod" and
|
||||
"IOMethod." These terms refer to the named structs inside the [repository
|
||||
package][repository], which defines the categories of cchat types.
|
||||
|
||||
It is recommended that the [reference documentation for the repository
|
||||
package][repository] be used in conjunction with this reference documentation.
|
||||
|
||||
Some of the protocols defined here are inspired by [Wayland's wire
|
||||
protocol][wayland-wire-protocol].
|
||||
|
||||
[repository]: https://godoc.org/github.com/diamondburned/cchat/repository
|
||||
[wayland-wire-protocol]: https://wayland-book.com/protocol-design/wire-protocol.html
|
||||
|
||||
## Protocol
|
||||
|
||||
This section specifies the communication encoding and such. It does not talk
|
||||
about possible means of communication.
|
||||
|
||||
The encoding of choice for this would be line-delimited JSON. That is, each
|
||||
implementation would read until a literal new line.
|
||||
|
||||
Note that this reference will mostly include a Go struct that define a JSON
|
||||
structure. If `json.RawMessage` is used, then it implies different types to be
|
||||
defined later in the documentation.
|
||||
|
||||
### Payload
|
||||
|
||||
Each JSON message is called a payload. Each payload must follow a certain
|
||||
structure, except for the arbitrary data field. JSON fields must always be
|
||||
["MixedCaps"-cased][MixedCaps] with ["Initialisms"][initialisms] to match
|
||||
one-on-one with Go struct field names.
|
||||
|
||||
Below is the structure of the JSON payload, represented in a Go struct.
|
||||
|
||||
```go
|
||||
type Payload struct {
|
||||
OP OP `json:"OP"`
|
||||
ID ObjectID `json:"ID"`
|
||||
Data json.RawMessage `json:"Data"` // Any type.
|
||||
}
|
||||
```
|
||||
|
||||
[MixedCaps]: https://golang.org/doc/effective_go.html#mixed-caps
|
||||
[initialisms]: https://github.com/golang/go/wiki/CodeReviewComments#initialisms
|
||||
|
||||
### Payload Type
|
||||
|
||||
The payload type separates one JSON message into two possible types: requests
|
||||
and events. As it is defined, events are sent from the backend to the frontend,
|
||||
and requests are sent from the frontend to the backend.
|
||||
|
||||
Two examples of this would be: a request to call a method, in which the frontend
|
||||
will reply with returns for said methods, and an event sent from the backend
|
||||
when a container's method needs to be called.
|
||||
|
||||
### Object ID
|
||||
|
||||
// TODO: THIS DOES NOT COVER OBJECT CREATION.
|
||||
|
||||
A payload must always have an object ID to denote the object that the payload
|
||||
belongs to. The ID is always of type uint32 to be compatible with languages that
|
||||
don't have unsigned types or any integer types in general. IDs will always be
|
||||
generated by the backend, and ID generation implementations could be arbitrary
|
||||
as long as it could guarantee uniqueness across the service globally.
|
||||
|
||||
An example implementation of ID generation could be a library with a global
|
||||
singleton, where a new ID is derived for each new object by atomically
|
||||
incrementing it by 1.
|
||||
|
||||
Implementations of ID generation are allowed to free up unused IDs, that is, for
|
||||
discarded objects, their IDs can be reused. Note that this is only possible when
|
||||
both sides agree on the disposal of those objects.
|
||||
|
||||
As an example of discarding old IDs: if the backend overrides a list of Servers
|
||||
with a completely new list of Servers, then the frontend must discard the old
|
||||
list in favor of new ones. At this point, either side could reason that the IDs
|
||||
of those old Servers are now unused, so the backend should be able to reuse that
|
||||
ID. Note that this requires a proper job queue to be implemented in the frontend
|
||||
as well as proper synchronization primitives in the backend. A job queue is
|
||||
required to ensure that the IDs are properly discarded before new objects with
|
||||
the same IDs are sent over.
|
||||
|
||||
For concreteness, below is the ID type defined in Go syntax.
|
||||
|
||||
```go
|
||||
type ID uint32
|
||||
```
|
||||
|
||||
Here's a naive Go implementation of atomic ID generation without any reusing.
|
||||
|
||||
```go
|
||||
var currentID uint32
|
||||
|
||||
// GenerateID returns a new unique ID. This function is safe for concurrent use.
|
||||
func GenerateID() uint32 {
|
||||
return atomic.AddUint32(¤tID, 1)
|
||||
}
|
||||
```
|
||||
|
||||
### Documenting Payload Types and Examples
|
||||
|
||||
For documentation purposes, when a stream of traffic is written down, JSON
|
||||
objects that are prefixed with `<-` denotes an event, and `->` denotes a
|
||||
request.
|
||||
|
||||
Also, for brevity, JSON will mostly be formatted to span across multiple lines.
|
||||
In reality, as the protocol strictly uses new lines (`\n`, LF only) to delimit
|
||||
JSON messages, everything will all be squeezed into a single line. There will
|
||||
be no indicative arrows.
|
||||
|
||||
### Struct Messages
|
||||
|
||||
The behavior for all defined structs is for them to be completely marshaled into
|
||||
JSON. As all defined structs only have exported fields, this shouldn't be a
|
||||
problem.
|
||||
|
||||
### Method Behaviors
|
||||
|
||||
All GetterMethods with no parameters and AsserterMethods of an interface will be
|
||||
resolved immediately right before sending said interface. As such, these methods
|
||||
cannot be manually called using requests.
|
||||
|
||||
### OP Code
|
||||
|
||||
Each payload will have an OP code, which is just a
|
||||
[SCREAMING_SNAKE_CASE][case-lists] string that sits in the `"OP"` field. Each
|
||||
payload type has its own set of possible OP codes.
|
||||
|
||||
For concreteness and examples, below is a formal definition of the OP code type.
|
||||
|
||||
```go
|
||||
type OP string
|
||||
```
|
||||
|
||||
[case-lists]: https://en.wikipedia.org/wiki/Naming_convention_(programming)#Examples_of_multiple-word_identifier_formats
|
||||
|
||||
#### Event OP codes
|
||||
|
||||
These OP codes are only sent from the backend to the frontend.
|
||||
|
||||
```go
|
||||
const (
|
||||
// OPSet is used on SetterMethods, which are the Set methods in Container
|
||||
// interfaces.
|
||||
OPSet = "SET"
|
||||
// OPReturn is used for returns of IO methods. The structure of the data
|
||||
// field is described in the Return Events section.
|
||||
OPReturn = "RETURN"
|
||||
)
|
||||
```
|
||||
|
||||
#### Request OP codes
|
||||
|
||||
These OP codes are only sent from the frontend to the backend.
|
||||
|
||||
```go
|
||||
const (
|
||||
// OPCallMethod is used when the frontend needs to call a method in an
|
||||
// interface.
|
||||
OPCallMethod = "CALL_METHOD"
|
||||
// OPCallContainerMethod is used when the frontend needs to call a
|
||||
// ContainerMethod. Although this OP is also used to call a method, it is
|
||||
// separated because it's called differently.
|
||||
OPCallContainerMethod = "CALL_CONTAINER_METHOD"
|
||||
)
|
||||
```
|
||||
|
||||
### Request Messages
|
||||
|
||||
As mentioned above, all messages sent from the client are called requests. Each
|
||||
request will ask the server to do something based on the given OP code, and the
|
||||
given ID will be used to determine what object the request refers to. This same
|
||||
ID will be used in replies from the backend to obtain the same object. As such,
|
||||
instead of identifying individual requests with an ID, we identify individual
|
||||
objects with an ID instead.
|
||||
|
||||
#### Calling a Method
|
||||
|
||||
The frontend could send a request to the backend to ask for a method to be
|
||||
called. Note that, as the [Method Behaviors](#method-behaviors) section points
|
||||
out, certain methods cannot be called, because they're already called
|
||||
beforehand. Any other method type not listed in that section must be explicitly
|
||||
called.
|
||||
|
||||
This section does NOT apply to ContainerMethods. For this method type, refer to
|
||||
[Calling a Container Method][#calling-a-container-method].
|
||||
|
||||
To call a method, a payload must use the `CALL_METHOD` OP, and the data field
|
||||
must follow a concrete structure. The object ID must be a valid ID that points
|
||||
to the right object to call the method on.
|
||||
|
||||
Below concretely defines the structure of the Data field in Go struct.
|
||||
|
||||
```go
|
||||
type CallRequestData struct {
|
||||
Parameters []json.RawMessage `json:"Parameters"`
|
||||
}
|
||||
```
|
||||
|
||||
Here's an example of the frontend requesting to call the `Do` method inside
|
||||
`Actioner`, which is an IOMethod. For reference, its function signature is
|
||||
`Do(string, cchat.ID) error`.
|
||||
|
||||
For reference, this example assumes that the `Actioner` object has the object ID
|
||||
ofo `31341`.
|
||||
|
||||
```json
|
||||
-> {
|
||||
"OP": "CALL_METHOD",
|
||||
"ID": 31341,
|
||||
"Data": {
|
||||
"Parameters": ["print_message_id", "767517199165292574"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Calling a Container Method
|
||||
|
||||
### Event Messages
|
||||
|
||||
#### Return Messages from Events
|
||||
|
||||
All incoming events that are replies of GetterMethod (with parameters) and
|
||||
IOMethod calls will have OPReturn and a specific structure for the data field:
|
||||
|
||||
```go
|
||||
type ReturnData struct {
|
||||
// Returns represent arbitrary values. Its length should be the same as
|
||||
// the Returns field of GetterMethod, or length 1 if it's an IOMethod.
|
||||
Returns []json.RawMessage `json:"Returns"`
|
||||
Error string `json:"Error,omitempty"` // optional field
|
||||
}
|
||||
```
|
||||
|
||||
// TODO: THIS SECTION DOES NOT ELABORATE ON CONTAINER TYPES.
|
||||
|
||||
Here's an example of `RawContent(cchat.ID, string) (string, error)` (a
|
||||
GetterMethod) returning successfully with no error:
|
||||
|
||||
```json
|
||||
<- {
|
||||
"OP": "RETURN",
|
||||
"Data": {
|
||||
"Returns": ["edited message content"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Here's the same method returning an error:
|
||||
|
||||
```json
|
||||
<- {
|
||||
"OP": "RETURN",
|
||||
"Data": {
|
||||
"Returns": null,
|
||||
"Error": "failed to parse ID: given argument is not a valid integer"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Here's an example of Actioner's `Actions(cchat.ID) []string` (a GetterMethod)
|
||||
returning successfully:
|
||||
|
||||
```json
|
||||
<- {
|
||||
"OP": "RETURN",
|
||||
"ID": 31341,
|
||||
"Data": {
|
||||
"Returns": [["delete_message", "print_message_id"]]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Here's an example of Actioner's `Do(string, cchat.ID) error` (an IOMethod)
|
||||
returning successfully:
|
||||
|
||||
```json
|
||||
<- {
|
||||
"OP": "RETURN",
|
||||
"ID": 31341,
|
||||
"Data": {
|
||||
"Returns": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note that since this method does not return any value and only an error, the
|
||||
value of `Returns` will always be `null`. This is different from a method
|
||||
intentionally returning `nil`, as the JSON value will be `[null]` instead,
|
||||
denoting that the type of `Returns` is indeed an array with `null` as a value.
|
||||
|
||||
# Implementation
|
||||
|
||||
## Backend
|
||||
|
||||
1. Implement onto cchat interfaces normally.
|
||||
2. Make a main entrypoint package that imports adapter.
|
||||
|
||||
```go
|
||||
func init() {
|
||||
services.RegisterService(&discord.Services{})
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Main is a shortcut to Start with []cchat.Service from services.Get().
|
||||
adapter.Main(os.Args)
|
||||
}
|
||||
```
|
||||
|
||||
3. Install this compiled main binary into `~/.config/cchat/services`.
|
||||
|
||||
## Frontend
|
||||
|
||||
1. Dash-import `frontend`.
|
|
@ -0,0 +1,29 @@
|
|||
// Package adapter provides functions to convert typical cchat calls to
|
||||
// RPC-compatible payloads.
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat/services"
|
||||
)
|
||||
|
||||
// Main is a helper function that calls Start() wtih the services from
|
||||
// services.Get().
|
||||
func Main() error {
|
||||
s, errs := services.Get()
|
||||
if errs != nil {
|
||||
for _, err := range errs {
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Start(s)
|
||||
}
|
||||
|
||||
func Start(services []cchat.Service) error {
|
||||
panic("Implement me")
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
// Package frontend
|
||||
package frontend
|
||||
|
||||
import "github.com/diamondburned/cchat/services"
|
||||
|
||||
func init() {
|
||||
services.RegisterSource(loadPlugins)
|
||||
}
|
||||
|
||||
func loadPlugins() []error {
|
||||
panic("Implement me")
|
||||
|
||||
// d, err := ioutil.ReadDir("stuff")
|
||||
// // err check
|
||||
|
||||
// for _, info := range d {
|
||||
// f, err := os.Open(filepath.Join("stuff", file.Name()))
|
||||
// // err check
|
||||
|
||||
// services.RegisterService(f)
|
||||
// }
|
||||
}
|
Loading…
Reference in New Issue