Get rid of setTimeout calls in qwebchannel.js client code.
[qt:qtwebchannel.git] / src / webchannel / qwebchannel.js
1 /****************************************************************************
2 **
3 ** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies).
4 ** Copyright (C) 2014 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff <milian.wolff@kdab.com>
5 ** Contact: http://www.qt-project.org/legal
6 **
7 ** This file is part of the QtWebChannel module of the Qt Toolkit.
8 **
9 ** $QT_BEGIN_LICENSE:LGPL$
10 ** Commercial License Usage
11 ** Licensees holding valid commercial Qt licenses may use this file in
12 ** accordance with the commercial license agreement provided with the
13 ** Software or, alternatively, in accordance with the terms contained in
14 ** a written agreement between you and Digia.  For licensing terms and
15 ** conditions see http://qt.digia.com/licensing.  For further information
16 ** use the contact form at http://qt.digia.com/contact-us.
17 **
18 ** GNU Lesser General Public License Usage
19 ** Alternatively, this file may be used under the terms of the GNU Lesser
20 ** General Public License version 2.1 as published by the Free Software
21 ** Foundation and appearing in the file LICENSE.LGPL included in the
22 ** packaging of this file.  Please review the following information to
23 ** ensure the GNU Lesser General Public License version 2.1 requirements
24 ** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
25 **
26 ** In addition, as a special exception, Digia gives you certain additional
27 ** rights.  These rights are described in the Digia Qt LGPL Exception
28 ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
29 **
30 ** GNU General Public License Usage
31 ** Alternatively, this file may be used under the terms of the GNU
32 ** General Public License version 3.0 as published by the Free Software
33 ** Foundation and appearing in the file LICENSE.GPL included in the
34 ** packaging of this file.  Please review the following information to
35 ** ensure the GNU General Public License version 3.0 requirements will be
36 ** met: http://www.gnu.org/copyleft/gpl.html.
37 **
38 **
39 ** $QT_END_LICENSE$
40 **
41 ****************************************************************************/
42
43 "use strict";
44
45 var QWebChannelMessageTypes = {
46     signal: 1,
47     propertyUpdate: 2,
48     init: 3,
49     idle: 4,
50     debug: 5,
51     invokeMethod: 6,
52     connectToSignal: 7,
53     disconnectFromSignal: 8,
54     setProperty: 9,
55     response: 10,
56 };
57
58 var QWebChannel = function(transport, initCallback)
59 {
60     if (typeof transport !== "object" || typeof transport.send !== "function") {
61         console.error("The QWebChannel expects a transport object with a send function and onmessage callback property." +
62                       " Given is: transport: " + typeof(transport) + ", transport.send: " + typeof(transport.send));
63         return;
64     }
65
66     var channel = this;
67     this.transport = transport;
68
69     this.send = function(data)
70     {
71         if (typeof(data) !== "string") {
72             data = JSON.stringify(data);
73         }
74         channel.transport.send(data);
75     }
76
77     this.transport.onmessage = function(message)
78     {
79         var data = message.data;
80         if (typeof data === "string") {
81             data = JSON.parse(data);
82         }
83         switch (data.type) {
84             case QWebChannelMessageTypes.signal:
85                 channel.handleSignal(data);
86                 break;
87             case QWebChannelMessageTypes.response:
88                 channel.handleResponse(data);
89                 break;
90             case QWebChannelMessageTypes.propertyUpdate:
91                 channel.handlePropertyUpdate(data);
92                 break;
93             case QWebChannelMessageTypes.init:
94                 channel.handleInit(data);
95                 break;
96             default:
97                 console.error("invalid message received:", message.data);
98                 break;
99         }
100     }
101
102     this.execCallbacks = {};
103     this.execId = 0;
104     this.exec = function(data, callback)
105     {
106         if (!callback) {
107             // if no callback is given, send directly
108             channel.send(data);
109             return;
110         }
111         if (channel.execId === Number.MAX_VALUE) {
112             // wrap
113             channel.execId = Number.MIN_VALUE;
114         }
115         if (data.hasOwnProperty("id")) {
116             console.error("Cannot exec message with property id: " + JSON.stringify(data));
117             return;
118         }
119         data.id = channel.execId++;
120         channel.execCallbacks[data.id] = callback;
121         channel.send(data);
122     };
123
124     this.objects = {};
125
126     this.handleSignal = function(message)
127     {
128         var object = channel.objects[message.object];
129         if (object) {
130             object.signalEmitted(message.signal, message.args);
131         } else {
132             console.warn("Unhandled signal: " + message.object + "::" + message.signal);
133         }
134     }
135
136     this.handleResponse = function(message)
137     {
138         if (!message.hasOwnProperty("id")) {
139             console.error("Invalid response message received: ", JSON.stringify(message));
140             return;
141         }
142         channel.execCallbacks[message.id](message.data);
143         delete channel.execCallbacks[message.id];
144     }
145
146     this.handlePropertyUpdate = function(message)
147     {
148         for (var i in message.data) {
149             var data = message.data[i];
150             var object = channel.objects[data.object];
151             if (object) {
152                 object.propertyUpdate(data.signals, data.properties);
153             } else {
154                 console.warn("Unhandled property update: " + data.object + "::" + data.signal);
155             }
156         }
157         channel.exec({type: QWebChannelMessageTypes.idle});
158     }
159
160     // prevent multiple initialization which might happen with multiple webchannel clients.
161     this.initialized = false;
162     this.handleInit = function(message)
163     {
164         if (channel.initialized) {
165             return;
166         }
167         channel.initialized = true;
168         for (var objectName in message.data) {
169             var data = message.data[objectName];
170             var object = new QObject(objectName, data, channel);
171         }
172         if (initCallback) {
173             initCallback(channel);
174         }
175         channel.exec({type: QWebChannelMessageTypes.idle});
176     }
177
178     this.debug = function(message)
179     {
180         channel.send({type: QWebChannelMessageTypes.debug, data: message});
181     };
182
183     channel.exec({type: QWebChannelMessageTypes.init});
184 };
185
186 function QObject(name, data, webChannel)
187 {
188     this.__id__ = name;
189     webChannel.objects[name] = this;
190
191     // List of callbacks that get invoked upon signal emission
192     this.__objectSignals__ = {};
193
194     // Cache of all properties, updated when a notify signal is emitted
195     this.__propertyCache__ = {};
196
197     var object = this;
198
199     // ----------------------------------------------------------------------
200
201     function unwrapQObject( response )
202     {
203         if (!response
204             || !response["__QObject*__"]
205             || response["id"] === undefined
206             || response["data"] === undefined) {
207             return response;
208         }
209         var objectId = response.id;
210         if (webChannel.objects[objectId])
211             return webChannel.objects[objectId];
212
213         var qObject = new QObject( objectId, response.data, webChannel );
214         qObject.destroyed.connect(function() {
215             if (webChannel.objects[objectId] === qObject) {
216                 delete webChannel.objects[objectId];
217                 // reset the now deleted QObject to an empty {} object
218                 // just assigning {} though would not have the desired effect, but the
219                 // below also ensures all external references will see the empty map
220                 // NOTE: this detour is necessary to workaround QTBUG-40021
221                 var propertyNames = [];
222                 for (var propertyName in qObject) {
223                     propertyNames.push(propertyName);
224                 }
225                 for (var idx in propertyNames) {
226                     delete qObject[propertyNames[idx]];
227                 }
228             }
229         });
230         return qObject;
231     }
232
233     function addSignal(signalData, isPropertyNotifySignal)
234     {
235         var signalName = signalData[0];
236         var signalIndex = signalData[1];
237         object[signalName] = {
238             connect: function(callback) {
239                 if (typeof(callback) !== "function") {
240                     console.error("Bad callback given to connect to signal " + signalName);
241                     return;
242                 }
243
244                 object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || [];
245                 object.__objectSignals__[signalIndex].push(callback);
246
247                 if (!isPropertyNotifySignal) {
248                     // only required for "pure" signals, handled separately for properties in propertyUpdate
249                     webChannel.exec({
250                         type: QWebChannelMessageTypes.connectToSignal,
251                         object: object.__id__,
252                         signal: signalIndex
253                     });
254                 }
255             },
256             disconnect: function(callback) {
257                 if (typeof(callback) !== "function") {
258                     console.error("Bad callback given to disconnect from signal " + signalName);
259                     return;
260                 }
261                 object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || [];
262                 var idx = object.__objectSignals__[signalIndex].indexOf(callback);
263                 if (idx === -1) {
264                     console.error("Cannot find connection of signal " + signalName + " to " + callback.name);
265                     return;
266                 }
267                 object.__objectSignals__[signalIndex].splice(idx, 1);
268                 if (!isPropertyNotifySignal && object.__objectSignals__[signalIndex].length === 0) {
269                     // only required for "pure" signals, handled separately for properties in propertyUpdate
270                     webChannel.exec({
271                         type: QWebChannelMessageTypes.disconnectFromSignal,
272                         object: object.__id__,
273                         signal: signalIndex
274                     });
275                 }
276             }
277         };
278     }
279
280     /**
281      * Invokes all callbacks for the given signalname. Also works for property notify callbacks.
282      */
283     function invokeSignalCallbacks(signalName, signalArgs)
284     {
285         var connections = object.__objectSignals__[signalName];
286         if (connections) {
287             connections.forEach(function(callback) {
288                 callback.apply(callback, signalArgs);
289             });
290         }
291     }
292
293     this.propertyUpdate = function(signals, propertyMap)
294     {
295         // update property cache
296         for (var propertyIndex in propertyMap) {
297             var propertyValue = propertyMap[propertyIndex];
298             object.__propertyCache__[propertyIndex] = propertyValue;
299         }
300
301         for (var signalName in signals) {
302             // Invoke all callbacks, as signalEmitted() does not. This ensures the
303             // property cache is updated before the callbacks are invoked.
304             invokeSignalCallbacks(signalName, signals[signalName]);
305         }
306     }
307
308     this.signalEmitted = function(signalName, signalArgs)
309     {
310         invokeSignalCallbacks(signalName, signalArgs);
311     }
312
313     function addMethod(methodData)
314     {
315         var methodName = methodData[0];
316         var methodIdx = methodData[1];
317         object[methodName] = function() {
318             var args = [];
319             var callback;
320             for (var i = 0; i < arguments.length; ++i) {
321                 if (typeof arguments[i] === "function")
322                     callback = arguments[i];
323                 else
324                     args.push(arguments[i]);
325             }
326
327             webChannel.exec({
328                 "type": QWebChannelMessageTypes.invokeMethod,
329                 "object": object.__id__,
330                 "method": methodIdx,
331                 "args": args
332             }, function(response) {
333                 if (response !== undefined) {
334                     var result = unwrapQObject(response);
335                     if (callback) {
336                         (callback)(result);
337                     }
338                 }
339             });
340         };
341     }
342
343     function bindGetterSetter(propertyInfo)
344     {
345         var propertyIndex = propertyInfo[0];
346         var propertyName = propertyInfo[1];
347         var notifySignalData = propertyInfo[2];
348         // initialize property cache with current value
349         object.__propertyCache__[propertyIndex] = propertyInfo[3];
350
351         if (notifySignalData) {
352             if (notifySignalData[0] === 1) {
353                 // signal name is optimized away, reconstruct the actual name
354                 notifySignalData[0] = propertyName + "Changed";
355             }
356             addSignal(notifySignalData, true);
357         }
358
359         Object.defineProperty(object, propertyName, {
360             get: function () {
361                 var propertyValue = object.__propertyCache__[propertyIndex];
362                 if (propertyValue === undefined) {
363                     // This shouldn't happen
364                     console.warn("Undefined value in property cache for property \"" + propertyName + "\" in object " + object.__id__);
365                 }
366
367                 return propertyValue;
368             },
369             set: function(value) {
370                 if (value === undefined) {
371                     console.warn("Property setter for " + propertyName + " called with undefined value!");
372                     return;
373                 }
374                 object.__propertyCache__[propertyIndex] = value;
375                 webChannel.exec({
376                     "type": QWebChannelMessageTypes.setProperty,
377                     "object": object.__id__,
378                     "property": propertyIndex,
379                     "value": value
380                 });
381             }
382         });
383
384     }
385
386     // ----------------------------------------------------------------------
387
388     data.methods.forEach(addMethod);
389
390     data.properties.forEach(bindGetterSetter);
391
392     data.signals.forEach(function(signal) { addSignal(signal, false); });
393
394     for (var name in data.enums) {
395         object[name] = data.enums[name];
396     }
397 }
398
399 //required for use with nodejs
400 if (typeof module === 'object') {
401     module.exports = {
402         QWebChannel: QWebChannel
403     };
404 }