WIP rpc framework

pull/1/head
ansuz 7 years ago
parent 4377f2cee7
commit 38bd27303b

@ -0,0 +1,7 @@
<!DOCTYPE html>
<html>
<head>
<script src="respond.js"></script>
</head>
<body>

@ -0,0 +1,147 @@
(function () {
var Frame = {};
var uid = function () {
return Number(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER))
.toString(32).replace(/\./g, '');
};
// create an invisible iframe with a given source
// append it to a parent element
// execute a callback when it has loaded
Frame.create = function (parent, src, onload, timeout) {
var iframe = document.createElement('iframe');
timeout = timeout || 10000;
var to = window.setTimeout(function () {
onload('[timeoutError] could not load iframe at ' + src);
}, timeout);
iframe.setAttribute('id', 'cors-store');
iframe.onload = function (e) {
onload(void 0, iframe, e);
window.clearTimeout(to);
};
// We must pass a unique parameter here to avoid cache problems in Firefox with
// the NoScript plugin: if the iframe's content is taken from the cache, the JS
// is not executed with NoScript....
iframe.setAttribute('src', src + '?t=' + new Date().getTime());
iframe.style.display = 'none';
parent.appendChild(iframe);
};
/* given an iframe with an rpc script loaded, create a frame object
with an asynchronous 'send' method */
Frame.open = function (e, A, timeout) {
var win = e.contentWindow;
var frame = {};
frame.id = uid();
var listeners = {};
var timeouts = {};
timeout = timeout || 5000;
frame.accepts = function (o) {
return A.some(function (e) {
switch (typeof(e)) {
case 'string': return e === o;
case 'object': return e.test(o);
}
});
};
var changeHandlers = frame.changeHandlers = [];
frame.change = function (f) {
if (typeof(f) !== 'function') {
throw new Error('[Frame.change] expected callback');
}
changeHandlers.push(f);
};
var _listener = function (e) {
if (!frame.accepts(e.origin)) {
console.log("message from %s rejected!", e.origin);
return;
}
var message = JSON.parse(e.data);
var uid = message._uid;
var error = message.error;
var data = message.data;
if (!uid) {
console.log("No uid!");
return;
}
if (uid === 'change' && changeHandlers.length) {
changeHandlers.forEach(function (f) {
f(data);
});
return;
}
if (timeouts[uid]) {
window.clearTimeout(timeouts[uid]);
}
if (listeners[uid]) {
listeners[uid](error, data, e);
delete listeners[uid];
}
};
window.addEventListener('message', _listener);
frame.close = function () {
window.removeEventListener('message', _listener);
};
/* method (string): (set|get|remove)
key (string)
data (string)
cb (function) */
frame.send = function (method, content, cb) {
var req = {
method: method,
//key: key,
data: content, //data,
};
var id = req._uid = uid();
// uid must not equal 'change'
while(id === 'change') {
id = req._uid = uid();
}
if (typeof(cb) === 'function') {
//console.log("setting callback!");
listeners[id] = cb;
//console.log("setting timeout of %sms", timeout);
timeouts[id] = window.setTimeout(function () {
// when the callback is executed it will clear this timeout
cb('[TimeoutError] request timed out after ' + timeout + 'ms');
}, timeout);
} else {
console.log(typeof(cb));
}
win.postMessage(JSON.stringify(req), '*');
};
return frame;
};
if (typeof(module) !== 'undefined' && module.exports) {
module.exports = Frame;
} else if (typeof(define) === 'function' && define.amd) {
define(['jquery'], function () {
return Frame;
});
} else {
window.Frame = Frame;
}
}());

@ -0,0 +1,32 @@
var validDomains = [ /.*/i, ];
var isValidDomain = function (o) {
return validDomains.some(function (e) {
switch (typeof(e)) {
case 'string': return e === o;
case 'object': return e.test(o);
}
});
};
window.addEventListener('message', function(e) {
if (!isValidDomain(e.origin)) { return; }
var payload = JSON.parse(e.data);
var parent = window.parent;
var respond = function (error, data) {
var res = {
_uid: payload._uid,
error: error,
data: data,
};
parent.postMessage(JSON.stringify(res), '*');
};
//console.error(payload);
switch(payload.method) {
case undefined:
return respond('No method supplied');
default:
return respond(void 0, "EHLO");
}
});

@ -249,7 +249,6 @@ define([
} }
}; };
var evt = Util.mkEvent(); var evt = Util.mkEvent();
var respond = function (e, out) { var respond = function (e, out) {
evt.fire(e, out); evt.fire(e, out);
@ -259,9 +258,8 @@ define([
try { try {
var parsed = JSON.parse(raw); var parsed = JSON.parse(raw);
var txid = parsed.txid; var txid = parsed.txid;
var message = parsed.message;
setTimeout(function () { setTimeout(function () {
service(message.command, message.content, function (e, result) { service(parsed.q, parsed.content, function (e, result) {
respond(JSON.stringify({ respond(JSON.stringify({
txid: txid, txid: txid,
error: e, error: e,
@ -285,33 +283,56 @@ define([
}); });
}, "Test rpc factory"); }, "Test rpc factory");
/*
assert(function (cb) { assert(function (cb) {
var getBlob = function (url, cb) { require([
var xhr = new XMLHttpRequest(); '/assert/frame/frame.js',
xhr.open("GET", url, true); ], function (Frame) {
xhr.responseType = "blob"; Frame.create(document.body, '/assert/frame/frame.html', function (e, frame) {
xhr.onload = function () { if (e) { return cb(false); }
cb(void 0, this.response);
};
xhr.send();
};
var $img = $('img#thumb-orig'); var channel = Frame.open(frame, [
getBlob($img.attr('src'), function (e, blob) { /.*/i,
console.log(e, blob); ], 5000);
Thumb.fromImageBlob(blob, function (e, thumb) {
console.log(thumb); channel.send('HELO', null, function (e, res) {
var th = new Image(); if (res === 'EHLO') { return cb(true); }
th.src = URL.createObjectURL(thumb); cb(false);
th.onload = function () { });
$(document.body).append($(th).addClass('thumb'));
cb(th.width === Thumb.dimension && th.height === Thumb.dimension);
};
}); });
}); });
}, "PEWPEW");
(function () {
var guid = Wire.uid();
var t = Wire.tracker({
timeout: 1000,
hook: function (txid, q, content) {
console.info(JSON.stringify({
guid: guid,
txid: txid,
q: q,
content: content,
}));
},
});
assert(function (cb) {
t.call('SHOULD_TIMEOUT', null, function (e) {
if (e === 'TIMEOUT') { return cb(true); }
cb(false);
});
}, 'tracker should timeout');
assert(function (cb) {
var id = t.call('SHOULD_NOT_TIMEOUT', null, function (e, out) {
if (e) { return cb(false); }
if (out === 'YES') { return cb(true); }
cb(false);
}); });
*/ t.respond(id, void 0, 'YES');
}, "tracker should not timeout");
}());
Drive.test(assert); Drive.test(assert);

@ -7,25 +7,118 @@ define([
Requirements Requirements
* some transmission methods can be interrupted * [x] some transmission methods can be interrupted
* handle disconnects and reconnects * [x] handle disconnects and reconnects
* handle callbacks * [x] handle callbacks
* configurable timeout * [x] configurable timeout
* Service should expose 'addClient' method * [x] be able to answer only queries with a particular id
* and handle broadcast * be able to implement arbitrary listeners on the service-side
* and not call 'ready' until those listeners are ready
* identical API for:
* iframe postMessage
* server calls over netflux
* postMessage to webworker
* postMessage to sharedWorker
* on-wire protocol should actually be the same for rewriting purposes
* q
* guid (globally unique id)
* txid (message id)
* content
* be able to compose different RPCs as streams
* intercept and rewrite capacity
* multiplex multiple streams over one stream
* blind redirect
* intelligent router
* broadcast (with ACK?)
* message
*
*/ */
var uid = function () { var uid = Wire.uid = function () {
return Number(Math.floor(Math.random () * return Number(Math.floor(Math.random () *
Number.MAX_SAFE_INTEGER)).toString(32); Number.MAX_SAFE_INTEGER)).toString(32);
}; };
/* tracker(options)
maintains a registry of asynchronous function calls
allows you to:
hook each call to actually send to a remote service...
abort any call
trigger the pending callback with arguments
set the state of the tracker (active/inactive)
*/
Wire.tracker = function (opt) {
opt = opt || {};
var hook = opt.hook || function () {};
var timeout = opt.timeout || 5000;
var pending = {};
var timeouts = {};
var call = function (method, data, cb) {
var id = uid();
// if the callback is not invoked in time, time out
timeouts[id] = setTimeout(function () {
if (typeof(pending[id]) === 'function') {
cb("TIMEOUT");
delete pending[id];
return;
}
throw new Error('timed out without function to call');
}, timeout);
pending[id] = function () {
// invoke the function with arguments...
cb.apply(null, Array.prototype.slice.call(arguments));
// clear its timeout
clearTimeout(timeouts[id]);
// remove the function from pending
delete pending[id];
};
hook(id, method, data);
return id;
};
var respond = function (id, err, response) {
if (typeof(pending[id]) !== 'function') {
throw new Error('invoked non-existent callback');
}
pending[id](err, response);
};
var abort = function (id) {
if (pending[id]) {
clearTimeout(timeouts[id]);
delete pending[id];
return true;
}
return false;
};
var t = {
call: call,
respond: respond,
abort: abort,
state: true,
};
t.setState = function (active) {
t.state = Boolean(active);
};
return t;
};
/* /*
opt = { opt = {
timeout: 30000,
send: function () { send: function () {
}, },
@ -45,50 +138,44 @@ opt = {
}; };
*/ */
Wire.create = function (opt, cb) { var parseMessage = function (raw) {
var ctx = {}; try { return JSON.parse(raw); } catch (e) { return; }
var pending = ctx.pending = {}; };
ctx.connected = false;
var rpc = {};
Wire.create = function (opt, cb) {
opt.constructor(function (e, service) { opt.constructor(function (e, service) {
if (e) { return setTimeout(function () { cb(e); }); } if (e) { return setTimeout(function () { cb(e); }); }
var rpc = {};
rpc.send = function (type, data, cb) { var guid = Wire.uid();
var txid = uid(); var t = Wire.tracker({
if (typeof(cb) !== 'function') { timeout: opt.timeout,
throw new Error('expected callback'); hook: function (txid, q, content) {
}
ctx.pending[txid] = function (err, response) {
cb(err, response);
};
service.send(JSON.stringify({ service.send(JSON.stringify({
guid: guid,
q: q,
txid: txid, txid: txid,
message: { content: content,
command: type,
content: data,
},
})); }));
},
});
rpc.send = function (type, data, cb) {
t.call(type, data, cb);
}; };
service.receive(function (raw) { service.receive(function (raw) {
try { var data = parseMessage(raw);
var data = JSON.parse(raw); if (typeof(data) === 'undefined') {
var txid = data.txid; return console.error("UNHANDLED_MESSAGE", raw);
if (!txid) { throw new Error('NO_TXID'); } }
var cb = pending[txid]; if (!data.txid) { throw new Error('NO_TXID'); }
if (data.error) { return void cb(data.error); } t.respond(data.txid, data.error, data.content);
cb(void 0, data.content);
} catch (e) { console.error("UNHANDLED_MESSAGE", raw); }
}); });
cb(void 0, rpc); cb(void 0, rpc);
}); });
}; };
return Wire; return Wire;
}); });

Loading…
Cancel
Save