afb-apiset: Fix start of apis
[src/app-framework-binder.git] / docs / protocol-x-afb-ws-json1.md
1 # The websocket protocol x-afb-ws-json1
2
3 The WebSocket protocol *x-afb-ws-json1* is used to communicate between
4 an application and a binder. It allows access to all registered apis
5 of the binder.
6
7 This protocol is inspired from the protocol **OCPP - SRPC** as described for
8 example here:
9 [OCPP transport specification - SRPC over WebSocket](http://www.gir.fr/ocppjs/ocpp_srpc_spec.shtml).
10
11 The registration to the IANA is still to be done, see:
12 [WebSocket Protocol Registries](https://www.iana.org/assignments/websocket/websocket.xml)
13
14 This document gives a short description of the protocol *x-afb-ws-json1*.
15 A more formal description has to be done.
16
17 ## Architecture
18
19 The protocol is intended to be symmetric. It allows:
20
21 - to CALL a remote procedure that returns a result
22 - to push and receive EVENT
23
24 ## Messages
25
26 Valid messages are made of *text* frames that are all valid JSON.
27
28 Valid messages are:
29
30 Calls:
31
32 ```txt
33 [ 2, ID, PROCN, ARGS ]
34 [ 2, ID, PROCN, ARGS, TOKEN ]
35 ```
36
37 Replies (3: OK, 4: ERROR):
38
39 ```txt
40 [ 3, ID, RESP ]
41 [ 4, ID, RESP ]
42 ```
43
44 Events:
45
46 ```txt
47 [ 5, EVTN, OBJ ]
48 ```
49
50 Where:
51
52 | Field | Type   | Description
53 |-------|--------|------------------
54 | ID    | string | A string that identifies the call. A reply to that call use the ID of the CALL.
55 | PROCN | string | The procedure name to call of the form "api/verb"
56 | ARGS  | any    | Any argument to pass to the call (see afb_req_json that returns it)
57 | RESP  | any    | The response to the call
58 | TOKEN | string | The authorisation token
59 | EVTN  | string | Name of the event in the form "api/event"
60 | OBJ   | any    | The companion object of the event
61
62 Below, an example of exchange:
63
64 ```txt
65 C->S:   [2,"156","hello/ping",null]
66 S->C:   [3,"156",{"response":"Some String","jtype":"afb-reply","request":{"status":"success","info":"Ping Binder Daemon tag=pingSample count=1 query=\"null\"","uuid":"ec30120c-6997-4529-9d63-c0de0cce56c0"}}]
67 ```
68
69 ## History
70
71 ### 14 November 2019
72
73 Removal of token returning. The replies
74
75 ```txt
76 [ 3, ID, RESP, TOKEN ]
77 [ 4, ID, RESP, TOKEN ]
78 ```
79
80 are removed from the specification.
81
82 ## Future
83
84 Here are the planned extensions:
85
86 - add binary messages with cbor data
87 - add calls with unstructured replies
88
89 This could be implemented by extending the current protocol or by
90 allowing the binder to accept either protocol including the new ones.
91
92 ## Javascript implementation
93
94 The file **AFB.js** is a javascript implementation of the protocol.
95
96 Here is that code:
97
98 ```javascript
99 /*
100  * Copyright (C) 2017-2019 "IoT.bzh"
101  * Author: José Bollo <jose.bollo@iot.bzh>
102  *
103  * Licensed under the Apache License, Version 2.0 (the "License");
104  * you may not use this file except in compliance with the License.
105  * You may obtain a copy of the License at
106  *
107  *   http://www.apache.org/licenses/LICENSE-2.0
108  *
109  * Unless required by applicable law or agreed to in writing, software
110  * distributed under the License is distributed on an "AS IS" BASIS,
111  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
112  * See the License for the specific language governing permissions and
113  * limitations under the License.
114  */
115 AFB = function(base, initialtoken){
116
117 if (typeof base != "object")
118    base = { base: base, token: initialtoken };
119
120 var initial = {
121    base: base.base || "api",
122    token: base.token || initialtoken || "HELLO",
123    host: base.host || window.location.host,
124    url: base.url || undefined
125 };
126
127 var urlws = initial.url || "ws://"+initial.host+"/"+initial.base;
128
129 /*********************************************/
130 /****                                     ****/
131 /****             AFB_context             ****/
132 /****                                     ****/
133 /*********************************************/
134 var AFB_context;
135 {
136    var UUID = undefined;
137    var TOKEN = initial.token;
138
139    var context = function(token, uuid) {
140       this.token = token;
141       this.uuid = uuid;
142    }
143
144    context.prototype = {
145       get token() {return TOKEN;},
146       set token(tok) {if(tok) TOKEN=tok;},
147       get uuid() {return UUID;},
148       set uuid(id) {if(id) UUID=id;}
149    };
150
151    AFB_context = new context();
152 }
153 /*********************************************/
154 /****                                     ****/
155 /****             AFB_websocket           ****/
156 /****                                     ****/
157 /*********************************************/
158 var AFB_websocket;
159 {
160    var CALL = 2;
161    var RETOK = 3;
162    var RETERR = 4;
163    var EVENT = 5;
164
165    var PROTO1 = "x-afb-ws-json1";
166
167    AFB_websocket = function(on_open, on_abort) {
168       var u = urlws;
169       if (AFB_context.token) {
170          u = u + '?x-afb-token=' + AFB_context.token;
171          if (AFB_context.uuid)
172             u = u + '&x-afb-uuid=' + AFB_context.uuid;
173       }
174       this.ws = new WebSocket(u, [ PROTO1 ]);
175       this.url = u;
176       this.pendings = {};
177       this.awaitens = {};
178       this.counter = 0;
179       this.ws.onopen = onopen.bind(this);
180       this.ws.onerror = onerror.bind(this);
181       this.ws.onclose = onclose.bind(this);
182       this.ws.onmessage = onmessage.bind(this);
183       this.onopen = on_open;
184       this.onabort = on_abort;
185    }
186
187    function onerror(event) {
188       var f = this.onabort;
189       if (f) {
190          delete this.onopen;
191          delete this.onabort;
192          f && f(this);
193       }
194       this.onerror && this.onerror(this);
195    }
196
197    function onopen(event) {
198       var f = this.onopen;
199       delete this.onopen;
200       delete this.onabort;
201       f && f(this);
202    }
203
204    function onclose(event) {
205       for (var id in this.pendings) {
206          try { this.pendings[id][1](); } catch (x) {/*TODO?*/}
207       }
208       this.pendings = {};
209       this.onclose && this.onclose();
210    }
211
212    function fire(awaitens, name, data) {
213       var a = awaitens[name];
214       if (a)
215          a.forEach(function(handler){handler(data);});
216       var i = name.indexOf("/");
217       if (i >= 0) {
218          a = awaitens[name.substring(0,i)];
219          if (a)
220             a.forEach(function(handler){handler(data);});
221       }
222       a = awaitens["*"];
223       if (a)
224          a.forEach(function(handler){handler(data);});
225    }
226
227    function reply(pendings, id, ans, offset) {
228       if (id in pendings) {
229          var p = pendings[id];
230          delete pendings[id];
231          try { p[offset](ans); } catch (x) {/*TODO?*/}
232       }
233    }
234
235    function onmessage(event) {
236       var obj = JSON.parse(event.data);
237       var code = obj[0];
238       var id = obj[1];
239       var ans = obj[2];
240       AFB_context.token = obj[3];
241       switch (code) {
242       case RETOK:
243          reply(this.pendings, id, ans, 0);
244          break;
245       case RETERR:
246          reply(this.pendings, id, ans, 1);
247          break;
248       case EVENT:
249       default:
250          fire(this.awaitens, id, ans);
251          break;
252       }
253    }
254
255    function close() {
256       this.ws.close();
257       this.ws.onopen =
258       this.ws.onerror =
259       this.ws.onclose =
260       this.ws.onmessage =
261       this.onopen =
262       this.onabort = function(){};
263    }
264
265    function call(method, request, callid) {
266       return new Promise((function(resolve, reject){
267          var id, arr;
268          if (callid) {
269             id = String(callid);
270             if (id in this.pendings)
271                throw new Error("pending callid("+id+") exists");
272          } else {
273             do {
274                id = String(this.counter = 4095 & (this.counter + 1));
275             } while (id in this.pendings);
276          }
277          this.pendings[id] = [ resolve, reject ];
278          arr = [CALL, id, method, request ];
279          if (AFB_context.token) arr.push(AFB_context.token);
280          this.ws.send(JSON.stringify(arr));
281       }).bind(this));
282    }
283
284    function onevent(name, handler) {
285       var id = name;
286       var list = this.awaitens[id] || (this.awaitens[id] = []);
287       list.push(handler);
288    }
289
290    AFB_websocket.prototype = {
291       close: close,
292       call: call,
293       onevent: onevent
294    };
295 }
296 /*********************************************/
297 /****                                     ****/
298 /****                                     ****/
299 /****                                     ****/
300 /*********************************************/
301 return {
302    context: AFB_context,
303    ws: AFB_websocket
304 };
305 };
306 ```