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() {}
|
func (m method) internalMethod() {}
|
||||||
|
|
||||||
// GetterMethod is a method that returns a regular value. These methods must not
|
// 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 {
|
type GetterMethod struct {
|
||||||
method
|
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