Protocol¶
The Eider protocol starts with the JSON-RPC 1.0 specification and extends it to manage sessions, bridged connections, object and method marshalling, call cancellation, and alternative serialization formats.
Messages¶
Request¶
A request (method invocation) takes the following form:
{
"dst": 1,
"src": 2,
"id": 3,
"this": {
"__*__": 4,
"rsid": 5
},
"method": "answer",
"params": [
"life",
"the universe",
"everything"
]
}
-
dst
For calls that should be forwarded (bridged) to another peer, this is an integer identifying the destination connection. For direct calls, it should be
null
or not present.
-
src
For calls that have been forwarded (bridged) from another peer, this is an integer identifying the source connection. For direct calls, it should be
null
or not present.
-
id
This is an integer uniquely identifying the request. It may also be
null
or not present, in which case no response will be returned.
-
this
A reference to the object whose method is to be invoked (see object marshalling). For calls to the
LocalSessionManager
(e.g. open() and free()), this may benull
or not present.
-
method
A string identifying the method to be invoked.
-
params
An array of arguments to pass to the method. May be not present if there are no arguments.
Response¶
A response (method return) takes the following form:
{
"dst": 2,
"id": 3,
"result": 42
}
-
dst
For responses to forwarded (bridged) calls, this is an integer identifying the origin of the call (the
src
of the request becomes thedst
of the response). For direct responses, this should benull
or not present.
-
id
An integer identifying the request to which this response corresponds.
-
result
The return value of the call, or
null
if the method did not return a value. If this property is missing, Eider will interpret the message as an error response.
Error Response¶
If a method throws an exception, the response takes the following form:
{
"dst": 2,
"id": 3,
"error": {
"name": "TerribleGhastlyError",
"message": "Don't Panic"
}
}
-
dst
This has the same meaning as for successful responses.
-
id
This has the same meaning as for successful responses.
-
error
This is an object representing the thrown exception. At minimum, it should have
name
andmessage
string properties describing the type of error and any pertinent details. It may also have astack
string property with a stack trace (the format of which is implementation-specific).Eider implementations may attempt to use the
name
field to convert the exception to an appropriate native exception type before passing it to client code. They may also use thestack
field as appropriate to simulate exception chaining.
Cancellation Request¶
A request to cancel (i.e. abort) an outstanding method call takes the following form:
{
"dst": 1,
"src": 2,
"cancel": 3
}
-
dst
This has the same meaning as for method call requests.
-
src
This has the same meaning as for method call requests.
-
cancel
This is an integer identifying the request which the caller wishes to cancel.
Callees are not required to honor cancellation requests; they may still finish
the call and return a result or an error. However, such results and errors
will be ignored by the caller. There is no mechanism to acknowledge a
cancellation request; after sending it, the caller should not assume any
specific remote state was reached. The Future or Promise representing the
remote call will have its exception immediately set to
asyncio.CancelledError
(Python) or Eider.Errors.CancelledError
(JavaScript).
Serialization Formats¶
By default, Eider expects all text messages to be encoded in JSON and all
binary messages in MessagePack. The reference implementations also allow
alternative format(s) to be specified when creating a Connection
object;
however, the particular format(s) to be used must be either agreed upon in
advance or transmitted through some side-channel.
Eider also includes a mechanism for specifying an alternative format on a per-message basis. To do this, the message must be split into two parts: a header formatted in JSON (or the agreed-upon format, as above), and an arbitrarily-formatted body. These parts must be sent as separate WebSocket messages, one immediately after the other.
To distinguish a message header from a complete message, and to specify the
format used for the subsequent body, the message header object must contain a
format
field. This field should be a string identifying a serialization
format that the remote peer knows how to handle. The string "json"
should
be reserved for JSON and "msgpack"
for MessagePack.
When the format
field is present, the only other fields that the header
message should contain are dst
, src
, id
, and method
. The
this
, params
, result
, and error
fields are expected to be
contained in the body message instead. For example, the request above could be
transmitted as these two messages:
{
"dst": 1,
"src": 2,
"id": 3,
"method": "answer",
"format": "json"
}
{
"this": {
"__*__": 4,
"rsid": 5
},
"params": [
"life",
"the universe",
"everything"
]
}
And the response could be transmitted as these two messages:
{
"dst": 2,
"id": 3,
"format": "json"
}
{
"result": 42
}
If the method throws an exception, the response could be:
{
"dst": 2,
"id": 3,
"format": "json"
}
{
"error": {
"name": "TerribleGhastlyError",
"message": "Don't Panic"
}
}
Separating the header and body in this way yields an important benefit for
calls over a bridged session. Because all the information needed to forward
messages between two peers (i.e., dst
and src
) is contained within the
header, the bridging peer does not have to decode and re-encode the contents of
the message body when relaying a message.
Because it would quickly become tedious to have to specify the format for every
method call, the Connection.create_session()
method allows you to specify
an lformat
and rformat
to be used for all method calls and responses
for objects in a given session. The lformat
specifies how outgoing
messages will be encoded, and the rformat
is passed to the remote peer to
request how to encode its responses.
Marshalling References¶
In addition to “plain old data” (strings, numbers, null
, arrays/lists,
objects/dictionaries), the this
, params
, and result
fields of
requests and responses may contain references to objects, bound methods, and
bridged sessions.
References are represented as objects (dictionaries) containing a property
named "__*__"
, known as the object-id. The root object of each session
has null
as its object-id. For all other objects, the object-id is an
integer uniquely identifying it within its session.
The way references are encoded depends on the chosen serialization format. For
JSON, they are simply encoded “in-band” using the above representation. For
MessagePack, the representation is encoded and then wrapped in extension type
0
. This extra level of indirection makes MessagePack a safer choice if the
data is coming from an unknown source, because it eliminates the possibility of
the "__*__"
key colliding with plain old data.
Warning
When using JSON serialization, it is important to make sure that
plain data objects passed through Eider do not contain properties named
"__*__"
, as this may confuse the marshalling layer. If this cannot be
guaranteed, then use MessagePack or another serialization format that
provides a way to distinguish between data and object references.
The Eider implementations handle the details of marshalling (encoding) and unmarshalling (decoding) object references into and out of this representation.
Remote Objects¶
Objects residing on the remote peer (such as this
for a method call) are
represented like this:
{
"__*__": 1,
"rsid": 2
}
Here, rsid
is an integer uniquely identifying the remote session to which
the object belongs.
Local Objects¶
Similarly, objects residing on the local peer (such as the result of a
new_*
call, or a local reference passed for use as a callback) are
represented like this:
{
"__*__": 1,
"lsid": 2
}
where lsid
is an integer uniquely identifying the local session to which
the object belongs.
Bound Methods¶
References to bound methods of local and remote objects may also be included in
Eider messages. The representation of the frobnicate
method of a remote
object with object-id of 1
in remote session 2
would look like this:
{
"__*__": 1,
"rsid": 2,
"method": "frobnicate"
}
Change rsid
to lsid
to refer to a method of a local object instead.
Bridged Sessions¶
When a peer B creates a bridged session between peers A and C, it is passed back to peer A using this representation:
{
"__*__": 1,
"lsid": 2,
"bridge": {
"dst": 3,
"rsid": 4,
"lformat": "json"
}
}
The object-id and lsid
fields identify the bridge object on peer B,
used to manage the lifetime of the bridge. Within the bridge
field, the
dst
field identifies peer C, the rsid
field identifies the remote
session on peer C, and lformat
is the requested serialization format for
peer A to use when making calls or responding to callbacks.
Session Management¶
When a connection is first established, there are no remote sessions yet, and
therefore no remote objects with methods to call. With no methods to call, how
do you create a remote session? The answer is that every Eider connection
provides a special session (with lsid
of null
) whose root object
provides a special method:
-
LocalSessionManager.
open
(lsid, lformat=None)¶ Create a new session which may be subsequently identified with
lsid
. Method call responses and callbacks originating from this session will be encoded using the requestedlformat
.
It should not be necessary to call this method directly;
Connection.create_session()
will handle this for you.
A remote session is closed when its root object is released. Again, this
should not be done directly, but rather by calling RemoteSession.close()
or
using the session in a with
statement (Python 3.4), async with
statement (Python 3.5+), or Eider.using()
(JavaScript).
Native Objects¶
The null
session also provides a method that becomes important when passing
native objects and functions:
-
LocalSessionManager.
free
(lsid, loid)¶ Release the specified object. For instances of
LocalObject
, this is has the same effect as callingrelease()
. For native objects, which do not participate in Eider’s reference-counting protocol, this deletes the connection’s internal reference to the object. This method is called internally byRemoteObject._close()
to mask the difference betweenLocalObject
instances and native objects.