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