Skip to content

Instantly share code, notes, and snippets.

@humbletim
Last active November 12, 2016 03:41
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save humbletim/df41cd6598e93f79cc66f18328b863da to your computer and use it in GitHub Desktop.
Save humbletim/df41cd6598e93f79cc66f18328b863da to your computer and use it in GitHub Desktop.
HiFi EinsteinRosenBridge (EventBridgeAdapter)

... bridging the gap between EventBridge and its gaps ...

//
// EventBridgeAdapter -- a potentially easier way to use EventBridge
// 2016.08.01 humbletim
//
// This script is isomorphic -- so we detect which side we're running on and adapt accordingly.
if (typeof Script === 'object' && Script.include) {
// INTERFACE/CLIENT SIDE
var isHiFi = true;
var log = function() { print('[EventBridgeAdapter - hifi] ' + [].slice.call(arguments).join(' ')); };
} else {
// WEB SIDE
var isWeb = true;
var log = function() { console.info('[EventBridgeAdapter - web] ' + [].slice.call(arguments).join(' ')); };
var qwebchannel_src = 'qwebchannel.js'; //'qrc:///qtwebchannel/qwebchannel.js'
var _include = function(src, callback) {
var s = document.createElement('script');
s.src = src;
s.onload = function(evt) { callback(null, evt); };
s.onerror = function(evt) { callback(evt.message||evt, null); };
return (document.body || document.head).appendChild(s);
};
}
// Adapts gnarly EventBridge naming/calling conventions into something that duck-types as HTML5 MessagePort
function _EventBridgeMessagePort() {
var port = {
origin: isWeb ? 'web' : isHiFi ? 'hifi' : 'unknown',
// polarize the EventBridge names (based on current environment)
_names: (
isHiFi ? {
sendToOther: 'emitScriptEvent',
fromOther: 'webEventReceived'
} : {
sendToOther: 'emitWebEvent',
fromOther: 'scriptEventReceived'
}
),
_make_event: function(data, targetOrigin) {
return {
origin: this.origin,
data: data,
target: targetOrigin || '*',
tstamp: +new Date
};
},
_parse_event: function(raw) {
var event = raw.substr(0,1) === '{' ? JSON.parse(raw) : raw;
if (event && event.origin)
return event;
throw new Error('_parse_event event w/o origin');
},
PENDING: 0,
CONNECTED: 1,
STARTED: 2,
CLOSED: 3,
readyState: 0,
onreadystatechange: function() { log('port.readyState==', this.$readyState); },
onerror: function(e) { log('(default) port.onerror handler:', e); },
_send: function(event) {
port.emitEvent(JSON.stringify(event));
},
_receive: function(raw) {
try { port.onmessage(port._parse_event(raw)); }
catch(e) {
e.raw = raw;
port.onerror(e);
}
},
_queue: [],
start: function() {
if (port.readyState === port.CLOSED)
throw new Error('.start called when readyState==='+port.$readyState);
if (port.readyState !== port.STARTED) {
if (!port.eventBridge)
throw new Error('call .connect(eventBridge) first');
if (port.readyState !== port.CONNECTED)
throw new Error('.start expected readyState===CONNECTED but found readyState===' + port.$readyState);
port.readyState = port.STARTED;
try { port.onreadystatechange(port.readyState); }
catch(e) { log('.start -- error claling onreadystatechange:', e); port.onerror(e); }
if (port._queue.length) {
log('########### draining queue (#' + port._queue.length + ' messages)');
port._queue.splice(0, port._queue.length).forEach(port._send);
}
log('//started', port.$readyState);
return true;
} else
log('?? .start called when readState==' + port.$readyState, '(queue length is #'+port._queue.length+')');
},
postMessage: function(data, targetOrigin, transfer) {
if (port.readyState === port.CLOSED)
throw new Error('.postMessage called when readyState===' + port.$readyState);
// TODO: possibly support emulated 'transfer' parameter
if (transfer) throw new Error('postMessage third argument not yet supported');
var event = port._make_event(data, targetOrigin);
if (port.readyState === port.STARTED)
return port._send(event);
port._queue.push(event);
log('... queued message; readyState==', port.$readyState, 'queued.length==' + port._queue.length);
},
close: function(reason) {
if (port.readyState !== port.CLOSED) {
port.readyState = port.CLOSED;
port.onreadystatechange(port.readyState, reason);
}
if (port.receiveEvent) {
port.receiveEvent.disconnect(port._receive);
port.receiveEvent = port.emitEvent = port.eventBridge = null;
}
try { port.onclose(reason); }
catch(e) { log('close -- error calling onclose:', e); port.onerror(e); }
log('//closed', reason);
},
connect: function connect(eventBridge) {
if (port.readyState)
throw new Error('.connect called when readyState===' + port.$readyState);
port.emitEvent = eventBridge[port._names.sendToOther];
if (!port.emitEvent)
return port.onerror('.' + port._names.sendToOther + ' not found in ' + eventBridge);
port.receiveEvent = eventBridge[port._names.fromOther];
if (!port.receiveEvent)
return port.onerror('.' + port._names.fromOther +' signal not found in ' + eventBridge);
port.receiveEvent.connect(port._receive);
if (isWeb) {
window.addEventListener('beforeunload', function() {
log('onbeforeunload...', port.$readyState);
port.close('beforeunload');
});
} else if (isHiFi) {
Script.scriptEnding.connect(function() {
log('scriptEnding...', port.$readyState);
port.close('scriptEnding');
});
}
log(
'_connected', port.origin.toUpperCase(),
'send:', [port._names.sendToOther, typeof port.emitEvent],
'receive:', [port._names.fromOther, typeof port.receiveEvent]
);
port.eventBridge = eventBridge;
port.readyState = port.CONNECTED;
port.onreadystatechange(port.readyState);
try { port.onopen(port.readyState, eventBridge); }
catch(e) { log('connect -- error calling onopen:', e); port.onerror(e); }
return port;
}
};
// TODO: support port.addEventListener('message', etc.)
return Object.defineProperties(port, {
onmessage: {
configurable: true,
set: function(nv) {
// per HTML5 MessagePort conventions, assigning the .onmessage handler automatically ensures .start() is called
Object.defineProperty(port, 'onmessage', { configurable: true, value: nv });
if (port.readyState === port.CONNECTED) {
log('.onmessage assigned; calling .start()...');
port.start();
} else
log('.onmessage assigned; NOT calling .start() (.readyState == ' + port.$readyState + ')');
}
},
$readyState: { get: function() {
return Object.keys(port).filter(function(p) { return /^[A-Z]/.test(p) && port[p] === port.readyState && p; })+'';
} }
});
}
// Mini "Deferred" implementation; incomplete, but works similarly to jQuery.Deferred.
// * currently used as a way for shared methods to return an async callback placeholder
// * could switch to Promises, but that requires significant polyfills to work on Interface side
function _Deferred(beforeStart) {
if (!(this instanceof _Deferred))
return new _Deferred(beforeStart);
var dfd = this;
var $callback = function $callback(err, val) {
// normally callback will be replaced, but if failing in beforeStart need to memoize
log('early deferred callback...', err, val);
dfd.error = err;
dfd.value = val;
};
Object.defineProperty(dfd, '$callback', {
get: function() { return $callback; },
set: function(nv) {
$callback = nv;
// handle the case of early reject/resolve
if ('error' in dfd || 'value' in dfd) {
log('early rapid-fire $callback', dfd.error, dfd.value);
$callback(dfd.error, dfd.value);
}
}
});
dfd._callback = function(err, val) { log('_callback', err, val); dfd.$callback(err, val); };
dfd.resolve = function(val) { dfd._callback(null, val); };
dfd.reject = function(err) { dfd._callback(err, null); };
try { beforeStart && beforeStart(dfd); }
catch(e) { dfd.reject(e); }
}
// bootstrap an eventBridge object by whatever means are necessary to do so
// supporting the following scenarios:
// [ WebWindow, OverlayWebWindow, "OverlayWebWindowEx" prototype, testingStubs ]
function _openEventBridge(maybeEventBridge, qwebchannel_src, callback) {
// if the first argument duck-types as an EventBridge, use that
if (maybeEventBridge && typeof maybeEventBridge === 'object' && "emitScriptEvent" in maybeEventBridge)
return callback(null, maybeEventBridge); // hifi-side and web-side WebWindow
// we only have other options to try on the Web side
if (!isWeb)
return callback(new Error('unknown EventBridge scenario...'), null);
// first make sure this is a QWebChannel candidate scenario
if (typeof qt !== 'object')
return callback(new Error('typeof qt !== "object" (expected to be running in a Qt >= 5.5.1 WebChannel-enabled WebEngineView...)'));
// if not already available then include qwebchannel.js and provision afterwards
if (typeof QWebChannel !== 'function') {
log('-------------------------- including QWebChannel from:', qwebchannel_src);
return _include(qwebchannel_src, provision);
}
// QWebChannel is already available, should be able to provision immediately
return provision();
function provision(err, evt) {
var WebChannel = new QWebChannel(qt.webChannelTransport, function (channel) {
EventBridge = WebChannel.objects.eventBridge || // "OverlayWebWindowEx"
WebChannel.objects.eventBridgeWrapper.eventBridge; // OverlayWebWindow
callback(err, EventBridge);
});
_openEventBridge.WebChannel = WebChannel; // expose for debugging
}
}
// WIP
function _EventBridgeAdapter(window, options) {
var bridge = new _EventBridgeMessagePort(options);
options.debug && log('_EventBridgeAdapter -- bridge.readyState: ' + bridge.$readyState);
var adapter = new _EinsteinRosenChannel(bridge, options);
try {
_openEventBridge(
options.eventBridge || window.eventBridge || window.EventBridge || window,
options.qwebchannel_src || qwebchannel_src,
function(err, eventBridge) {
if (err) throw err;
bridge.connect(eventBridge);
}
);
} catch(e) {
log('_openEventBridge error: ', e);
isWeb && window.setTimeout(adapter.onerror.bind(adapter, e));
}
options.debug && log('//adapter created:', adapter);
return adapter;
}
// creates a new channel and wires to/from MessagePort-like peer
// options:
// version: earmark indicating local version
// shared: { methods: function() {} } -- auto-bridged methods
// onload: (optional) event handler callbacks
// onopen: (optional) ""
// onclose: (optional) ""
// onerror: (optional) ""
// key: (optional) earmark indicating a local key hint
// debug: (optional) enable/disable more verbose logging
// qwebchannel_src: (optional) URL to pull qwebchannel.js from (if/when needed)
//
function _EinsteinRosenChannel(peer, options) {
var self = {
peer: peer,
options: options,
_meta: {
origin: isWeb ? 'web' : isHiFi ? 'hifi' : 'unknown',
version: options.version,
key: options.key,
methods: Object.keys(options.shared||{}).filter(function(k) { return typeof options.shared[k] === 'function' })
},
debug: 'debug' in options ? options.debug : (isWeb ? /\bdebug\b/.test(window.location) : false),
toString: function() {
var async = this.async || {};
return '[EinsteinRosenChannel'+
' self='+this._meta.origin+'@'+this._meta.version+
' peer='+async.origin+'@'+async.version+
']';
},
onerror: options.onerror || function(err) { throw new Error(err); },
onload: options.onload || function() {},
onopen: options.onopen || function() {},
onclose: options.onclose || function() {},
onmessage: options.onmessage || function(event) { self.debug,1 && log('(default .onmessage)', event); },
Deferred: _Deferred,
shared: options.shared,
// note: JSON-P-like rpc invocations get attached to .callbacks (and can be inspected to help debug any messaging issues)
callbacks: {
CLOSED: function(event) {
log('received CLOSED event from peer side; reason:', event.data.reason);
self.close('peer:'+event.data.reason);
}
},
_handshake: function() {
log('handshaking w/', this.peer.origin, typeof this.peer.eventBridge);
// notify "other" side about "our" available shared methods
this.peer.postMessage({ rpc: 'HELO', args:[this._meta], callback: !this.async && 'HELO' }, this.peer.origin);
},
_become_friends: function(msg) {
log('HELO friend!!');
var _asyncProto = (msg.args && msg.args[0]) || {};
var async = self.async = Object.create(_asyncProto);
log('-- HELO', async.key, async.version, async.loadresult, async.methods);
async.methods = async.methods || [];
async.methods.forEach(function(name) {
log('[proxy] .async.'+name);
async[name] = function() {
var args = [].slice.call(arguments);
var out = { rpc: name };
if ('function' === typeof args[args.length-1]) {
var callback = args.pop();
out.callback = 'rpc-'+name+'-'+Math.random().toString(36).substr(2);
self.callbacks[out.callback] = function(event) {
delete self.callbacks[out.callback];
log(name, 'callback!', JSON.stringify(event));
callback.call(async, event.data.error, event.data.args && event.data.args[0]);
};
}
out.args = args;
self.postMessage(out, peer.origin);
};
});
// emulate async versions of the locally-defined shared functions too
// this allows calling sync "self.shared.css(...)" as async "self.async.css(..., function(err, val) { ... })"
Object.keys(options.shared).forEach(function(name) {
log('[local] .async.'+name);
_asyncProto[name] = function() {
log('note: invoking shared local func as async: ' + name);
var args = [].slice.call(arguments);
var error, result;
if ('function' === typeof args[args.length-1])
var callback = args.pop();
try {
result = options.shared[name].apply(async, args);
} catch(e) {
error = e+'';
log('virtual async error: ', name, error);
}
if (callback) {
if (result && typeof result === 'object' && result instanceof _Deferred)
result.$callback = callback;
else
callback.call(async, error, result);
}
};
});
var result = JSON.parse(JSON.stringify(self._meta));
try { result.loadresult = self.onload(self.async) }
catch(e) {
log('self.onload error:', e, e.lineNumber, e.message, e.stack);
result.loadresult = e + '';
}
return result;
},
_onmessage: function(event) {
try {
var result, error;
var msg = event.data;
self.debug && log('_receive:', event.origin, '->', event.target, JSON.stringify(msg));
if (msg.rpc) {
if (msg.rpc === 'HELO') {
result = self._become_friends(msg);
} else {
if (self.callbacks[msg.rpc]) {
self.debug && log('...calling self.callbacks.'+msg.rpc);
try { result = self.callbacks[msg.rpc](event); } catch(e) { error = e+''; }
} else if (options.shared[msg.rpc]) {
self.debug && log('...calling self.shared.'+msg.rpc);
try { result = options.shared[msg.rpc].apply(self.async, msg.args); } catch(e) { error = e+''; }
} else
error = new Error('shared method not found: ' + msg.rpc)+'';
if (error)
log('_receive.rpc error: ' + error);
}
if (msg.callback) {
var postBack = function(err, val) {
err && log('...postBack includes an error message:', err);
if (err === null || err === undefined)
err = undefined;
else
err = err+'';
self.postMessage({ rpc: msg.callback, error: err, args:[val] }, event.origin);
};
if (error)
log('_receive msg.callback early error: '+ error);
if (result && typeof result === 'object' && result instanceof _Deferred)
result.$callback = postBack;
else
postBack(error, result);
}
} else {
log('_receive !msg.rpc', JSON.stringify(event));
self.onmessage(event);
}
} catch(e) {
log('_receive error:', e, e.lineNumber, JSON.stringify(event));
}
},
close: function(reason) {
if (peer.readyState !== peer.CLOSED) {
peer.postMessage({ rpc: 'CLOSED', reason: reason }, peer.origin);
delete self.async;
peer.close(reason);
self.onclose && self.onclose(reason);
}
}
};
peer.onopen = function(readyState) {
log('peer.onopen! wiring handlers', peer.eventBridge);
self.postMessage = peer.postMessage; // OUTPUT: Adapter ==> EventBridge
peer.onmessage = self._onmessage; // INPUT: EventBridge ==> Adapter
try { self.onopen(readyState); }
catch(e) { log('self.onopen error: ', e); self.onerror(e); }
self._handshake();
};
peer.onclose = function(reason) {
log('peer.onclose! unwiring handlers');
self.postMessage = null;
peer.onmessage = null;
self.close(reason || 'peer-side closed');
};
peer.onerror = function(err) {
log('peer.onerror: ', err);
if (err.raw)
self.onmessage(err.raw);
else
self.onerror(err);
};
// if already connected then onopen isn't going to be fired...
if (peer.readyState) {
log('-==-=-=-=-==-=-=--==-=- peer already connected, manually calling onopen');
log('-==-=-=-=-==-=-=--==-=- peer already connected, manually calling onopen');
log('-==-=-=-=-==-=-=--==-=- peer already connected, manually calling onopen');
//peer.onopen(peer.readyState);
}
return Object.defineProperties(self, {
$readyState: { get: function() { return peer.$readyState; } }
});
}
// global "exports"
// FIXME: switch back to simple name once local refactoring is complete
//EventBridgeAdapter = _EventBridgeAdapter;
EinsteinRosenBridge = _EventBridgeAdapter;
/****************************************************************************
**
** Copyright (C) 2015 The Qt Company Ltd.
** Copyright (C) 2014 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff <milian.wolff@kdab.com>
** Contact: http://www.qt.io/licensing/
**
** This file is part of the QtWebChannel module of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL21$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see http://www.qt.io/terms-conditions. For further
** information use the contact form at http://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 2.1 or version 3 as published by the Free
** Software Foundation and appearing in the file LICENSE.LGPLv21 and
** LICENSE.LGPLv3 included in the packaging of this file. Please review the
** following information to ensure the GNU Lesser General Public License
** requirements will be met: https://www.gnu.org/licenses/lgpl.html and
** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
** As a special exception, The Qt Company gives you certain additional
** rights. These rights are described in The Qt Company LGPL Exception
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
** $QT_END_LICENSE$
**
****************************************************************************/
"use strict";
var QWebChannelMessageTypes = {
signal: 1,
propertyUpdate: 2,
init: 3,
idle: 4,
debug: 5,
invokeMethod: 6,
connectToSignal: 7,
disconnectFromSignal: 8,
setProperty: 9,
response: 10,
};
var QWebChannel = function(transport, initCallback)
{
if (typeof transport !== "object" || typeof transport.send !== "function") {
console.error("The QWebChannel expects a transport object with a send function and onmessage callback property." +
" Given is: transport: " + typeof(transport) + ", transport.send: " + typeof(transport.send));
return;
}
var channel = this;
this.transport = transport;
this.send = function(data)
{
if (typeof(data) !== "string") {
data = JSON.stringify(data);
}
channel.transport.send(data);
}
this.transport.onmessage = function(message)
{
var data = message.data;
if (typeof data === "string") {
data = JSON.parse(data);
}
switch (data.type) {
case QWebChannelMessageTypes.signal:
channel.handleSignal(data);
break;
case QWebChannelMessageTypes.response:
channel.handleResponse(data);
break;
case QWebChannelMessageTypes.propertyUpdate:
channel.handlePropertyUpdate(data);
break;
default:
console.error("invalid message received:", message.data);
break;
}
}
this.execCallbacks = {};
this.execId = 0;
this.exec = function(data, callback)
{
if (!callback) {
// if no callback is given, send directly
channel.send(data);
return;
}
if (channel.execId === Number.MAX_VALUE) {
// wrap
channel.execId = Number.MIN_VALUE;
}
if (data.hasOwnProperty("id")) {
console.error("Cannot exec message with property id: " + JSON.stringify(data));
return;
}
data.id = channel.execId++;
channel.execCallbacks[data.id] = callback;
channel.send(data);
};
this.objects = {};
this.handleSignal = function(message)
{
var object = channel.objects[message.object];
if (object) {
object.signalEmitted(message.signal, message.args);
} else {
console.warn("Unhandled signal: " + message.object + "::" + message.signal);
}
}
this.handleResponse = function(message)
{
if (!message.hasOwnProperty("id")) {
console.error("Invalid response message received: ", JSON.stringify(message));
return;
}
channel.execCallbacks[message.id](message.data);
delete channel.execCallbacks[message.id];
}
this.handlePropertyUpdate = function(message)
{
for (var i in message.data) {
var data = message.data[i];
var object = channel.objects[data.object];
if (object) {
object.propertyUpdate(data.signals, data.properties);
} else {
console.warn("Unhandled property update: " + data.object + "::" + data.signal);
}
}
channel.exec({type: QWebChannelMessageTypes.idle});
}
this.debug = function(message)
{
channel.send({type: QWebChannelMessageTypes.debug, data: message});
};
channel.exec({type: QWebChannelMessageTypes.init}, function(data) {
for (var objectName in data) {
var object = new QObject(objectName, data[objectName], channel);
}
// now unwrap properties, which might reference other registered objects
for (var objectName in channel.objects) {
channel.objects[objectName].unwrapProperties();
}
if (initCallback) {
initCallback(channel);
}
channel.exec({type: QWebChannelMessageTypes.idle});
});
};
function QObject(name, data, webChannel)
{
this.__id__ = name;
webChannel.objects[name] = this;
// List of callbacks that get invoked upon signal emission
this.__objectSignals__ = {};
// Cache of all properties, updated when a notify signal is emitted
this.__propertyCache__ = {};
var object = this;
// ----------------------------------------------------------------------
this.unwrapQObject = function(response)
{
if (response instanceof Array) {
// support list of objects
var ret = new Array(response.length);
for (var i = 0; i < response.length; ++i) {
ret[i] = object.unwrapQObject(response[i]);
}
return ret;
}
if (!response
|| !response["__QObject*__"]
|| response["id"] === undefined) {
return response;
}
var objectId = response.id;
if (webChannel.objects[objectId])
return webChannel.objects[objectId];
if (!response.data) {
console.error("Cannot unwrap unknown QObject " + objectId + " without data.");
return;
}
var qObject = new QObject( objectId, response.data, webChannel );
qObject.destroyed.connect(function() {
if (webChannel.objects[objectId] === qObject) {
delete webChannel.objects[objectId];
// reset the now deleted QObject to an empty {} object
// just assigning {} though would not have the desired effect, but the
// below also ensures all external references will see the empty map
// NOTE: this detour is necessary to workaround QTBUG-40021
var propertyNames = [];
for (var propertyName in qObject) {
propertyNames.push(propertyName);
}
for (var idx in propertyNames) {
delete qObject[propertyNames[idx]];
}
}
});
// here we are already initialized, and thus must directly unwrap the properties
qObject.unwrapProperties();
return qObject;
}
this.unwrapProperties = function()
{
for (var propertyIdx in object.__propertyCache__) {
object.__propertyCache__[propertyIdx] = object.unwrapQObject(object.__propertyCache__[propertyIdx]);
}
}
function addSignal(signalData, isPropertyNotifySignal)
{
var signalName = signalData[0];
var signalIndex = signalData[1];
object[signalName] = {
connect: function(callback) {
if (typeof(callback) !== "function") {
console.error("Bad callback given to connect to signal " + signalName);
return;
}
object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || [];
object.__objectSignals__[signalIndex].push(callback);
if (!isPropertyNotifySignal && signalName !== "destroyed") {
// only required for "pure" signals, handled separately for properties in propertyUpdate
// also note that we always get notified about the destroyed signal
webChannel.exec({
type: QWebChannelMessageTypes.connectToSignal,
object: object.__id__,
signal: signalIndex
});
}
},
disconnect: function(callback) {
if (typeof(callback) !== "function") {
console.error("Bad callback given to disconnect from signal " + signalName);
return;
}
object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || [];
var idx = object.__objectSignals__[signalIndex].indexOf(callback);
if (idx === -1) {
console.error("Cannot find connection of signal " + signalName + " to " + callback.name);
return;
}
object.__objectSignals__[signalIndex].splice(idx, 1);
if (!isPropertyNotifySignal && object.__objectSignals__[signalIndex].length === 0) {
// only required for "pure" signals, handled separately for properties in propertyUpdate
webChannel.exec({
type: QWebChannelMessageTypes.disconnectFromSignal,
object: object.__id__,
signal: signalIndex
});
}
}
};
}
/**
* Invokes all callbacks for the given signalname. Also works for property notify callbacks.
*/
function invokeSignalCallbacks(signalName, signalArgs)
{
var connections = object.__objectSignals__[signalName];
if (connections) {
connections.forEach(function(callback) {
callback.apply(callback, signalArgs);
});
}
}
this.propertyUpdate = function(signals, propertyMap)
{
// update property cache
for (var propertyIndex in propertyMap) {
var propertyValue = propertyMap[propertyIndex];
object.__propertyCache__[propertyIndex] = propertyValue;
}
for (var signalName in signals) {
// Invoke all callbacks, as signalEmitted() does not. This ensures the
// property cache is updated before the callbacks are invoked.
invokeSignalCallbacks(signalName, signals[signalName]);
}
}
this.signalEmitted = function(signalName, signalArgs)
{
invokeSignalCallbacks(signalName, signalArgs);
}
function addMethod(methodData)
{
var methodName = methodData[0];
var methodIdx = methodData[1];
object[methodName] = function() {
var args = [];
var callback;
for (var i = 0; i < arguments.length; ++i) {
if (typeof arguments[i] === "function")
callback = arguments[i];
else
args.push(arguments[i]);
}
webChannel.exec({
"type": QWebChannelMessageTypes.invokeMethod,
"object": object.__id__,
"method": methodIdx,
"args": args
}, function(response) {
if (response !== undefined) {
var result = object.unwrapQObject(response);
if (callback) {
(callback)(result);
}
}
});
};
}
function bindGetterSetter(propertyInfo)
{
var propertyIndex = propertyInfo[0];
var propertyName = propertyInfo[1];
var notifySignalData = propertyInfo[2];
// initialize property cache with current value
// NOTE: if this is an object, it is not directly unwrapped as it might
// reference other QObject that we do not know yet
object.__propertyCache__[propertyIndex] = propertyInfo[3];
if (notifySignalData) {
if (notifySignalData[0] === 1) {
// signal name is optimized away, reconstruct the actual name
notifySignalData[0] = propertyName + "Changed";
}
addSignal(notifySignalData, true);
}
Object.defineProperty(object, propertyName, {
get: function () {
var propertyValue = object.__propertyCache__[propertyIndex];
if (propertyValue === undefined) {
// This shouldn't happen
console.warn("Undefined value in property cache for property \"" + propertyName + "\" in object " + object.__id__);
}
return propertyValue;
},
set: function(value) {
if (value === undefined) {
console.warn("Property setter for " + propertyName + " called with undefined value!");
return;
}
object.__propertyCache__[propertyIndex] = value;
webChannel.exec({
"type": QWebChannelMessageTypes.setProperty,
"object": object.__id__,
"property": propertyIndex,
"value": value
});
}
});
}
// ----------------------------------------------------------------------
data.methods.forEach(addMethod);
data.properties.forEach(bindGetterSetter);
data.signals.forEach(function(signal) { addSignal(signal, false); });
for (var name in data.enums) {
object[name] = data.enums[name];
}
}
//required for use with nodejs
if (typeof module === 'object') {
module.exports = {
QWebChannel: QWebChannel
};
}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>EinsteinRosenBridge - simple.html</title>
<script src="EventBridgeAdapter.js"></script>
<script src="https://code.jquery.com/jquery-2.1.4.min.js"></script>
<script src="https://cdn.rawgit.com/nnattawat/flip/master/dist/jquery.flip.min.js"></script>
<script>;
function log() { console.info('[simple.html] ' + [].slice.call(arguments).join(' ')); };
window.onerror = function(e) {
log('window.onerror: ' + e);
vislog && vislog('<font color=red>'+e+'</font>');
};
var port, hifi;
jQuery(document).ready(function() {
jQuery('.card').flip();
jQuery('button').prop('disabled', true);
port = new EinsteinRosenBridge(window, {
version: '0.0.0',
key: 'web-side' + location.hash,
// methods attached here will be available to the Interface side...
shared: {
// example: listen for Camera mode updates from Interface side
modeUpdated: function(mode) {
// vanilla HTML5
document.querySelector('#cameraMode').innerHTML = mode;
// jQuery
jQuery('.cameraModes button').removeClass('active');
jQuery('.cameraModes button.'+mode.replace(' ', '-')).addClass('active');
},
// example: allow Interface side to configure and/or flip a card w/jquery.flip
flip: function(opts) {
jQuery('.card').flip(opts||false);
if (opts && opts.axis)
jQuery('.card .axis').text(opts.axis);
},
// example: let Interface side apply css styles through jQuery
css: function(selector, k, v) {
jQuery(selector).css(k, v);
},
},
// onload gets called once both the bridge and its shared methods are connected
onload: function(async) {
vislog('port.onload: ' + this, async.key);
vislog('shared methods:', async.methods.join(' | '));
window.hifi = async;
// example: forward button clicks to the Interface side
jQuery('button')
.off('click.onload')
.on('click.onload', function(evt) {
hifi.onClick({
id: evt.target.id,
x: evt.x,
y: evt.y,
button: evt.button
});
});
// get current location from Interface side
hifi.getCurrentLocation(function(err, loc) {
jQuery('#loc').text(err || loc.href);
});
},
// log any errors during bridge setup
onerror: function(err) {
console.error('port.onerror:' + err);
vislog('<font color=red>error: ' + err + '</font>');
},
onopen: function(readyState) {
console.info('port.onopen:' + readyState);
jQuery('button').prop('disabled', false);
vislog('opened ', this.$readyState);
},
onclose: function(reason) {
jQuery('button').prop('disabled', true);
vislog('closed ', reason, this.$readyState);
},
// "unhandled" (ie: non-shared rpc events) will be passed along to onmessage
onmessage: function(event) {
vislog('unhandled message: ' + JSON.stringify(event));
},
});
// if browsertest appears in the hash then load and provision the offline testing stubs
if (/browsertest/.test(location.hash)) {
_include('testStubs.js', function() {
console.info('instrumenting testing stubs (trapping port.onerror)', port.$readyState);
testStubs.setup(port);
vislog('//testStubs instrumented');
});
}
});
</script>
<style>
body { margin: 0; padding: 0; font-family: sans; font-size: .8em; overflow: hidden; }
button { text-shadow: 0px 0px 5px white; }
button.active { background-color: #696; }
</style>
</head>
<body>
<fieldset class='cameraModes'>
<legend>Camera.mode</legend>
<button class='third-person' onclick='hifi.setCameraMode(this.innerText)'>third person</button>
<button class='first-person' onclick='hifi.setCameraMode(this.innerText)'>first person</button>
<b id='cameraMode' style='float:right'>...</b>
<br />
<small>(try changing the Camera mode using the View menu too)</i>
</fieldset>
<fieldset>
<legend>buttons (click to round-trip colorize via Interface)</legend>
<button id='button-1'>button 1</button>
<button id='button-2'>button 2</button>
<button id='button-3'>button 3</button>
<button id='button-4'>button 4</button>
</fieldset>
<div id='output'></div>
<script>
vislog = function() { output.innerHTML += [].slice.call(arguments).join(' ')+'<br />'; };
</script>
<!-- jQuery flip example -->
<style>
#cards { position: absolute; bottom: 0; width: 100%; height: 48px; overflow:hidden; color: white; background-color: black; -webkit-user-select: none; }
#cards .card { white-space: nowrap; height: 48px; line-height:48px; text-align: center; }
</style>
<div id='cards'>
<div class='card'>
<div class='front'>
<div style='background-size: contain; background-image: url(https://highfidelity.com/hf-logo-alpha.png)'>(click to flip on <b class='axis'>y</b>)</div>
</div>
<div class='back'>
<b id='loc'>...</b>
</div>
</div>
</div>
<!-- /jQuery flip example -->
</body>
</html>
//
// EinsteinRosenBridge -- Interface Client script example
// 2016.08.01 humbletim
//
Script.include(Script.resolvePath('EventBridgeAdapter.js'));
// 1. create your OverlayWebWindow per usual:
var window = new OverlayWebWindow({
title: 'EinsteinRosenBridge Web Window',
source: Script.resolvePath('simple.html'),
width: 480,
height: 240
});
////note: EinsteinRosenBridge in theory also works with classic WebWindows:
// var window = new WebWindow('WebWin', Script.resolvePath('simple.html#'), 480, 240) ||
// window.setVisible(true);
var webside; // shared Web-side methods
// 2. open a wormhole by passing the window and your options to EinsteinRosenBridge constructor:
var port = new EinsteinRosenBridge(window, {
version: '0.0.0',
key: 'interface-side',
// "shared methods" automatically become available to the Web side (see simple.html)
shared: {
// share access for configuring Camera.mode
setCameraMode: Camera.setModeString,
// share access to the current hifi://location
getCurrentLocation: function() {
return Window.location;
},
// callback for receiving DOM events
onClick: function(evt) {
// generates a random web color
function random_rgb() {
function r() { return ~~(Math.random()*0xff) };
return 'rgb('+[r(), r(), r()]+')';
}
print('onClick!', JSON.stringify(evt));
if (evt.id) {
// generate a random color locally
// ... and applied it remotely using a jQuery.css proxy (that simple.html provides)
webside.css('#'+evt.id, { backgroundColor: random_rgb() });
}
},
// example of experimental support for the Deferred "promise" pattern
asyncTest: function(x, y) {
// returning a port.Deferred lets you start some long-running or async operation
// ... and once finished later call either dfd.resolve(result) or dfd.reject(Error)
return port.Deferred(function(dfd) {
Script.setTimeout(function() {
dfd.resolve(x+y);
// dfd.reject(new Error('or could reject with an error'));
}, 1000);
});
}
},
onload: function(async) {
print('port.onload:' + this, async.key);
print('shared methods:', async.methods.join(' | '));
webside = async;
// wiring example: this keeps the Web side informed of all Camera mode changes
Camera.modeUpdated.connect(webside.modeUpdated);
webside.modeUpdated(Camera.mode); // and value at startup
// wiring example: switch the "axis" of the "jQuery flipper" (bottom) based on Camera mode
Camera.modeUpdated.connect(function(mode) {
webside.flip({ axis: /first/.test(mode) ? 'x' : 'y' });
});
},
onerror: function(err) { print('port.onerror:' + err); },
onopen: function(readyState) { print('port.onopen:' + readyState); },
onclose: function(reason) { print('port.onclose:' + reason); },
onmessage: function(event) { print('port.onmessage (unhandled):' + JSON.stringify(event)); },
});
window.closed.connect(Script, 'stop');
//
// EinsteinRosenBridge -- HTML Testing Stubs example
// 2016.08.01 humbletim
//
// ... these stubs are meant to be used when testing simple.html offling (ie: using a normal web browser)
testStubs = {
_sendScriptEvent: function() {},
recv: function(a,b) { this._sendScriptEvent(JSON.stringify(port._make_event(a,b))); },
// minimalist EventBridge mock
eventBridge: {
emitWebEvent: function(x) {
console.info('emitWebEvent', x);
var evt = port.peer._parse_event(x);
console.info('~emitWebEvent', evt);
if (evt.data.rpc) {
var err, result;
if (!testStubs[evt.data.rpc])
err = 'testStubs: ' + evt.data.rpc + ' method not stubbed yet';
else {
try {
result = testStubs[evt.data.rpc].apply(port.async, evt.data.args);
} catch(e) {
console.error('testStubs.'+evt.data.rpc, e);
err = e+'';
}
}
if (evt.data.callback && evt.data.callback !== 'HELO')
testStubs._sendScriptEvent(JSON.stringify(port.peer._make_event(
{ rpc: evt.data.callback, error: err, args: [result] },
evt.origin
)));
}
},
scriptEventReceived: {
connect: function(f) { testStubs._sendScriptEvent = f; }
}
},
// mocks of shared methods from the Interface-side
methods: ['onClick','setCameraMode','getCurrentLocation'],
onClick: function(evt) {
console.info('testStubs.onClick', evt);
jQuery('#'+evt.id).css('backgroundColor', '#'+Math.floor(Math.random()*16777215).toString(16));
},
setCameraMode: function(mode) {
console.info('testStubs.setCameraMode', mode);
port.async.modeUpdated(mode);
port.async.flip({ axis: /first/.test(mode) ? 'x' : 'y' });
},
getCurrentLocation: function() {
return { href: 'about:testStubs' };
},
// instrument the test stubs onto the provided port
setup: function(port) {
this.port = port;
var peer = port.peer;
peer.connect(this.eventBridge);
peer.onmessage(peer._make_event(
{ rpc:'HELO', args:[{ key:'testStubs', version:'0.0.0', methods: this.methods }]}
));
port.shared.modeUpdated('first person');
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment