... bridging the gap between EventBridge and its gaps ...
-
-
Save humbletim/df41cd6598e93f79cc66f18328b863da to your computer and use it in GitHub Desktop.
HiFi EinsteinRosenBridge (EventBridgeAdapter)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/**************************************************************************** | |
** | |
** 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 | |
}; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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'); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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