-
-
Notifications
You must be signed in to change notification settings - Fork 147
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Run computation in isolated context #339
Comments
Here are some random notes from my doodling on this issue: Attempt 1: Load KLIPSE in a WebWorkerThis approach fails pretty quickly. I'd like to load the script from inside the // worker.js
importScript( 'https://storage.googleapis.com/app.klipse.tech/plugin_prod/js/klipse_plugin.min.js' )
// `document` missing Attempt 2: Load KLIPSE in an iframeiframes have DOM access, so theoretically this approach should be feasible even if less performant than the WebWorker. I was able to get JavaScript running as I expected and was getting the messaging coordinated as I wanted, though that also came with some awkward accounting - to say the least. https://gist.github.com/dmsnell/929a33d222af31abbee6173d96a0fd1a When I got to other languages things started falling apart and I believe that this is based on the fact that while JavaScript runs its code through before I go too far trying to figure this out I'd like to step back and look at changing the KLIPSE code to try running its evaluation inside a Attempt 3: Replace
|
Attempt 4Having run into
The |
This issue is now published on WorksHub. If you would like to work on this issue you can |
If you successfully complete this Issue via WorskHub there's a $300 reward available. |
@dmsnell I could help integrate this, but I might propose a more elegant solution that would involve compressing the data behind the scenes using a combination of my Hamsters.js library and my lzwFlossRedux library. lzwFloss does lzw compression on the fly using a background worker thread, and also decompresses the data using a worker thread. This would allow you to keep your dom changes on the main thread, while compressing and obfuscating what you want to be secure. You can limit Hamsters.js on startup to use only 1 thread, or you can take advantage of multiple threads if you'd like to run loops/computations in parallel. Let me know what you think about this idea, if I get some spare time I might work on it myself and collect that $300 reward. https://github.com/austinksmith/lzwFlossRedux.js |
@austinksmith Sounds good. I would propose getting it working before getting it working with compression and backgrounding for performance reasons. These I think are the essential bits:
|
A user started working on this issue via WorksHub. |
@Verdinjoshua26 started working on this issue via WorksHub. |
A user started working on this issue via WorksHub. |
2 similar comments
A user started working on this issue via WorksHub. |
A user started working on this issue via WorksHub. |
@dmsnell I've solved this problem for you, how do I go about submitting it to javascript works for the bounty? What i have is 100% working bi-directional message passing from klipse when typing in the input editor box and getting a response back from the klipse worker matching what is expected. What isn't working is the following.
window.parent.postMessage(event.data, "*");
|
Just some notes by the way, it's typically bad practice to try to overwrite a native function ie. overwriting eval. Because eval is a native method using const might be a slick work around but it seems like the redefining of eval is a code smell, or bad practice ... or maybe it just FEELS wrong but either way it seems to be working in this case. I would look at refactoring possibly to make use of new Function which is just like eval but it creates a copy of the original function which can then be called later, or possibly instead of calling eval directly it could just call a klipse unique method, when then calls the invoke message function directly so the only eval called is within the iframe itself. I think there is a bunch of room for optimization here but given the bounty is $300 i am not able to invest that much time into it, I actually already thought of a solution to the "which box invoked the iframe" problem too but i'd have to play around with it a bit to see if it works. Either way I think using the Iframe is going to be the ideal solution for this particular piece of software because you are almost 100% dealing with dom manipulation and worker threads were not made to deal with that, its a pain but I've forced myself not to diverge from the official worker specification lays out and hacking workers to make dom manipulation work properly is not something I'm prepared to deal with. |
Will check it soon |
@austinksmith since you mentioned me here I'll just note that the bounty isn't mine and I'm not sure how to advise you other than following the instructions on WorksHub. Happy to chat about your approach if you want feedback though. |
@dmsnell I'm sure github should be fine if the solution works out. Here is klipse-frame.js const waitUntil = ( p ) => new Promise( resolve => {
const runner = () => {
const v = p();
if ( v ) {
resolve( v );
}
requestIdleCallback( runner );
}
runner();
} );
const findEditor = wrapper => waitUntil( () => {
const editorIndex = Object
.keys( window.klipse_editors )
.find(
i => window.klipse_editors[ i ].display.wrapper.parentNode.parentNode === wrapper
);
if ( null === editorIndex ) {
return false;
}
const editor = window.klipse_editors[ editorIndex ];
if ( null === editor ) {
return false;
}
return editor;
} );
document.addEventListener('DOMContentLoaded', async (event) => {
//Create new messsage channel to send messages from iframe to main window
let controlChannel = new MessageChannel();
//Use port1 as the parentPort for messages
window.parentPort = controlChannel.port1;
window.parentEventData = {};
//Await klipse loading before continuing, seems hacky using waitUntil method..
await waitUntil(() => !!window.klipse );
//Get dom element blocks
let blocks = document.getElementById('blocks');
//Set parentPort onMessage receive handler
//Called when creating a new block
parentPort.onmessage = async (event) => {
if ( event.data.action === 'new-block' ) {
//Parse incoming event
let { data: { mode }, ports } = event;
let id = Symbol();
// construct shadow DOM editor
let wrapper = document.createElement( 'div' );
let input = document.createElement( 'div' );
wrapper.appendChild( input );
blocks.appendChild( wrapper );
window.klipse.plugin.klipsify( input, window.klipse_settings, mode);
await waitUntil( () => !! window.klipse_editors );
window.evalPorts.set( id, ports[ 0 ] );
//Called when content in a block changes
ports[ 0 ].onmessage = async (event) => {
let queue = window.pushQueue.get( event.data ) || new Set();
queue.add( id );
window.pushQueue.set( event.data, queue );
let editor = await findEditor( wrapper );
console.log("KLIPSE FRAME RECEIVED MESSAGE!! ", event.data);
// editor.setValue(event.data);
window.parent.postMessage(event.data, "*");
}
}
}
//This returns successfully and console logs fine on main window
window.parent.postMessage( 'klipse-loaded', '*', [ controlChannel.port2 ] );
}, false ); Here is klipse-frame.html <link rel="stylesheet" href="https://storage.googleapis.com/app.klipse.tech/css/codemirror.css">
<script>
window.klipse_settings = {
eval_idle_ms: 1000,
selector_eval_js: '.eval-js',
selector_eval_markdown: '.eval-markdown',
selector_eval_ocaml: '.eval-ocaml',
};
const log = console.log.bind( console );
const oldEval = window.eval;
window.evalPorts = new Map();
window.pushQueue = new Map();
window.eval = source => {
const value = oldEval(source);
const ports = window.pushQueue.get( source ) || new Set();
[...ports].map((id) => {
window.evalPorts.get(id);
}).forEach((port) => {
port.postMessage(value);
});
window.pushQueue.delete(source);
return value;
}
</script>
<div id="blocks"></div>
<script src="https://storage.googleapis.com/app.klipse.tech/plugin/js/klipse_plugin.js"></script>
<script src="klipse-frame.js"></script> Here is index.js const getPort = () => new Promise( resolve => {
window.addEventListener( 'message', event => {
console.log("MAIN CHANNEL RECEIVED RESPONSE FROM KLIPSE FRAME!! ", event.data);
if ( event.data === 'klipse-loaded' ) {
resolve( event.ports[ 0 ] );
}
} );
const klipseFrame = document.createElement( 'iframe' );
klipseFrame.setAttribute( 'style', 'display: none;' );
klipseFrame.src = 'klipse-frame.html';
document.body.appendChild( klipseFrame );
} );
document.addEventListener('DOMContentLoaded', async () => {
const controlPort = await getPort();
const blocks = document.getElementById( 'blocks' );
const newBlockButton = document.getElementById( 'newBlockButton' );
const modeSelector = document.getElementById( 'modeSelector' );
newBlockButton.addEventListener( 'click', event => {
// create DOM editor and output
const container = document.createElement( 'div' );
const sourcebox = document.createElement( 'textarea' );
sourcebox.setAttribute( 'style', 'width: 80%; height: 300px; font-size: 16px;');
const outputBox = document.createElement( 'div' );
const outputPre = document.createElement( 'pre' );
const output = document.createElement( 'code' );
outputPre.appendChild( output );
outputBox.appendChild( outputPre );
container.appendChild( sourcebox );
container.appendChild( outputBox );
blocks.appendChild( container );
// connect editor to KLIPSE
const blockChannel = new MessageChannel();
//Post new block message to klipse frame
controlPort.postMessage({
action: 'new-block',
mode: modeSelector.value
}, [ blockChannel.port2 ]);
//Send new message to klipse frame on input change
sourcebox.addEventListener( 'input', (event) => {
blockChannel.port1.postMessage(sourcebox.value);
});
//Handle response back from klipse frame
blockChannel.port1.onmessage = event => {
console.log("BLOCK CHANNEL RECEIVED RESPONSE FROM KLIPSE FRAME!! ", event.data);
output.innerHTML = event.data;
}
}, false );
}, false ); Here is index.html <div id="blocks"></div>
<select id="modeSelector">
<option value="eval-javascript">JavaScript</option>
<option value="eval-markdown">Markdown</option>
<option value="eval-ocaml">OCaml</option>
</select>
<button id="newBlockButton">New Block</button>
<script src="index.js"></script>html As I mentioned previously the only thing I didn't finish implementing was guaranteeing the boxMessagePort is used when responding from the iframe, this should be super simple to fix by just changing the original message to be an object like {
sourceBox: this.sourcebox.id,
portNumber: 0,
inputValue: sourceBox.value
} Hopefully that makes sense but basically it just needs a little more sugar syntax to tie it all together and it should work |
thanks for your patience with me @austinksmith. can you help me understand what's different about your posting vs. what I shared in my iframe gist? is it mostly that you're sending back the data over the channel?
|
This solution doesn't use a worker its just using an iframe, but yes this is creating a new message channel pair for every new box created, however what your original solution lacks is sending the computed dom back to the main window. So basically you were treating the iframe dom as the main window dom, this won't work because the iframe is an isolated context. What you can do is send things like the dom element id of the box that sent the message, compute the new dom you want, send the new dom back to the main window, then apply the previously computed dom to the new box. Ultimately you need to return something from your iframe, and the one computation you wont be able to get around is the actual
Well no, that isn't what I set out to do. I'm not going to be able to find a solution for that for you since its kind of outside the scope of the request. Basically if it works for javascript you should be able to adapt it for clojure etc. but I recommend sticking with javascript to get everything working. The previous solution had a few errors, missing semi colons etc and wasn't handling the incoming message from each boxChannel, it was relying on messagePort 2 which was on the parent channel but not the box Channels, now every box channel is sending their messages to the iframe independently of one another. You just need to make sure you call the required klipse methods, precalculate your final dom result, return that result from the iframe to the original box channel that sent the message, and then update the innerHTML of that box with the result. |
The linked gist is for an iframe, as I mentioned above. I'm having a little trouble following you and I wonder if you are addressing two of my different approaches, since the one I asked about is the iframe and not the WebWorker.
In your approach (but not in your shared code?) is the same idea for the WebWorker (send back the DOM updates) but use an iframe so we can grab it from a local DOM node instead of relying on a DOM polyfill? If so, yeah, I think that's reasonable, but I'd want to play around with it and see how the isolation and communication works to say for sure.
JavaScript is the easy part, but this request is entirely that scope - running all the languages in isolation 😆
In my mind we can't single out one part of the issue (the easiest part) and claim it's solved. The hard bits for me when I worked on this and left those notes was making sure the data flow, as you describe here, works from end-to-end. That is the substance of what makes this hard. I agree that it seems like it should be trivial and you "just" have to do all those things, but "just" filling in all those details and getting it to work is something that's still unsolved despite the bounty. I was working on it for my own benefit and not for the bounty, but had to give it up when I got busier with other work. A big part of making this possible is for someone to dive into the evaluation flow and track down in the Clojure(Script) code where we ultimately pass compiled JS to For all the other languages though they go through a compilation process and then the output of that gets passed to an This was a puzzle to me why the other languages failed and if you can figure that out you should have a big clue on how to solve this. It's possible I overlooked something basic when I tried. |
@austinksmith Once your PR is approved and merged to master by me, I send an email to Functional Works and you get paid. |
Will have to wait a bit but the ShadowRealm proposal I think will give us the ability to run everything inside a true sandbox, enforced by the browser. |
@austinksmith Did this stall? Happy to help wrap it up!
@dmsnell My impression is they provide a clean environment but would still lock the render thread unlike iframe or worker? |
See also #100
Being able to run the compilation/computation inside a WebWorker or in an iframe would make it significantly easier to secure the scripts to guard against abuse and it would enhance the viewing experience by moving expensive or runaway calculations out of the UI thread.
After a conversation with @viebel he recommended trying to prototype by replacing
eval()
with an async version that channels messages through the frames/contexts. Something like this would do…If running inside an iframe we could support view-only DOM creation by sending
.innerHTML
across the frame as a string and then stiching it up on the host side.The text was updated successfully, but these errors were encountered: