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