WIP IPC draft

This commit is contained in:
diamondburned 2020-10-18 19:06:48 -07:00
parent 4c835a467b
commit b5dabacff4
4 changed files with 382 additions and 1 deletions

View File

@ -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

327
services/ipc/README.md Normal file
View File

@ -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(&currentID, 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`.

View File

@ -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")
}

View File

@ -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)
// }
}