koding / kite.js

Kite client in JavaScript
kite.koding.com
MIT License
75 stars 16 forks source link

KiteProtocol primer #63

Open usirin opened 6 years ago

usirin commented 6 years ago

This is gonna be rather long, so bear with me to the end, and sorry in advance.

Let's start with our old friend math kite, with only one method defined on it named square, which will take a number and it's gonna return square of that number:

// math-server.js
const { KiteServer } = require('kite.js')

const math = new KiteServer({
  name: 'math',
  auth: false,
  api: {
    square: function(x, done) {
      done(null, x * x)
    },
  },
})

math.listen(7780)

The client for corresponding server will connect to it like this:

const { Kite } = require('kite.js')

const kite = new Kite({
  // assuming that they are running on the same host.
  url: 'http://0.0.0.0:7780',
  autoReconnect: false,
  autoConnect: false,
})

kite.on('open', () => {
  kite.tell('square', 5).then(res => console.log('result:', res))
  // => result: 25
})

kite.connect()

So what exactly is happening here?

Under the hood, Kite uses dnode-protocol to make calls to the each side of the connection, but since dnode-protocol doesn't try to specify a structure on how to send messages with extra/meta information (kite info, authentication info, etc), we need to define our own structure.

It currently works like this:

Request from client:

{
  "arguments": [
    {
      "kite": {
        "id": "d43382a0-6b41-485c-8a44-48e5a851f302",
        "username": "anonymous",
        "environment": "browser-environment",
        "name": "math-remote",
        "version": "1.0.9",
        "region": "browser-region",
        "hostname": "browser-hostname"
      },
      "withArgs": 5,
      "responseCallback": "[Function]"
    }
  ],
  "callbacks": {
    "0": [
      "0",
      "responseCallback"
    ]
  },
  "links": [],
  "method": "square"
}

Response from server:

{
  "method": 0,
  "arguments": [
    {
      "error": null,
      "result": 25
    }
  ],
  "callbacks": {},
  "links": []
}

The way it works right now:

Problems with current implementation

1) Packed object has a withArgs property to define arguments even though the dnode-protocol defines an arguments object. 2) Packed object has a responseCallback as its callback resulting in always having callbacks: { '0': ['0', 'responseCallback'] } which means having to send a string as to define which callback is gonna be called each time. (More bytes over the wire) 3) withArgs is an array if there is more than one argument that needs to be passed, while it's a simple value without an array if there is only one argument needs passing (like our math kite's square method). This creates an unnecessary inconsistencies between requests, and therefor we need to handle this in a special way by checking if withArgs is an array or not. 4) The request is transformed into a regular dnode-protocol message before it reaches to the handler defined on the api, so even though we send the requester's kite info, the transformed object doesn't have this info. Meaning that the handlers cannot know who is making the request. 5) We are wrapping response in an object with a shape of {error, result}, this requires first wrapping on the sender side, and de-wrapping on the responder side, resulting in unnecessary computation, and also more bytes over the wire. 6) Both requests and responses are being handled by the same method, namely handleIncomingMessage, which violates Single Responsibility Pattern resulting in more complex code, therefore more confusion.

Solution Proposal

The shape of request/response after proposed changes

Request from client:

{
  "arguments": [
    5,
    "[Function]",
    {
      "kite": {
        "id": "645f0802-51fc-4961-bdd0-55aed0245eae",
        "username": "anonymous",
        "environment": "browser-environment",
        "name": "browser-kite",
        "version": "1.0.9",
        "region": "browser-region",
        "hostname": "browser-hostname"
      }
    }
  ],
  "callbacks": {
    "0": [
      "1"
    ]
  },
  "links": [],
  "method": "square"
}

Response from server:

{
  "method": 0,
  "arguments": [
    null,
    25
  ],
  "links": [],
  "callbacks": {}
}

The code needs to be written will be the same, except now the handlers will have information about the requester as their last argument:

// math-server.js
const { KiteServer } = require('kite.js')

const math = new KiteServer({
  name: 'math',
  auth: false,
  api: {
    square: function(x, done, { kite, authentication }) {
      console.log('kite info:', kite)
      // => kite info: {
      //   kite: {
      //     id: '645f0802-51fc-4961-bdd0-55aed0245eae',
      //     username: 'anonymous',
      //     environment: 'browser-environment',
      //     name: 'browser-kite',
      //     version: '1.0.9',
      //     region: 'browser-region',
      //     hostname: 'browser-hostname',
      //   },
      // }

      // send the response
      done(null, x * x)
    },
  },
})

This solves Problem 1, 2, 3, 4, 5 and the details of the implementation will try to address Problem 6 as well, but since this problem is mostly related with maintanence issues, it can be deferred to a later time.

Plan of action

With these changes the connection between js version of kites will be backwards compatible, but in order to be fully compatible and if we agree that this is the way to go, golang implementation will need to adapt to these changes as well.

Please let me know if there is something missing, or if I am assuming more than I should, so that we can come up with a proper protocol definition.

/cc @gokmen @cihangir @rjeczalik @szkl @ppknap @sinan