I've been working on something similar on and off for a while, and I've found the space to be fascinating and somewhat puzzling. Most of the first questions I try to ask about these in browser notebooks don't have clean answers. In no particular order:
- Do you have any ideas about solving logic errors? As it stands right now a while(true) loop crashes the page and consecutive re-openings of the page for a short while (in chrome). What about things like document.body.innerHTML = "" (you actually seem to handle this pretty gracefully, I'll see if I can poke more holes in this another time). For what it's worth none of the online notebooks I've seen have a satisfactory solution to infinite loops, loop timeouts are too blunt an instrument and crashing the tab can take the browser minutes to recover.
- Is there a particular reason you use eval rather than the new Function constructor? From what I've read using new Function is much more performant, and other than different scoping a better choice than eval. Can't find the link at the moment, but it was wrapped up in the mdn[1] design docs for their codebox examples.
- I see you're using the first codemirror 6[2] beta release, how are you liking it? I really enjoy the interface so far.
- Do you have any favorite resources or inspirations about why you went about building a hackable offline local first notebook environment? I particularly like the experiments at Ink and Switch[3]. (many related hn submissions). As well as webstrates[4].
My attempt at this game is to break out of the notebook style single column layout and embrace an art board style canvas, which is a rather radical idea in that it is not obvious what that should look like or how basic interactions like hierarchies or execution order should look, but fun to explore. I also desperately want to prevent crashing as a result of logic errors and workflow footguns(like deleting DOM elements or overwriting storage), to that end I have a separate storage of scripts to rebuild a "safe boot" interface, but there is more thinking to be done here.
I look forward to poking around your code some more soon. Thanks for posting.
Edited out a question about your motivations. I reread your comment and the about page and I realized my motivations are similar to yours. While observable doesn't have a few of your key points, it is a fantastic product. The reason I don't settle on it is that I'm interested in experimenting with the environment outside of a notebook-with-cells interface.
> As it stands right now a while(true) loop crashes the page
> [...] I also desperately want to prevent crashing as a result of logic errors
I could be wrong here, but I think the new Site Isolation stuff in Chromium[0] may mean that you can sandbox an iframe to a different origin, so that the main threads of the top frame and the iframe aren't synced. So the iframe where the code is running would freeze, but the main frame would still be responsive. Again, I may have misinterpreted this because I only read about it in passing, and it was a while ago.
A brief skim through that doc looks like it is about a kind of security that's not relevant to my project. I'm trying to protect the user from themselves while simultaneously giving the user full power over the document.
Web workers are interesting and relevant to this use case, however the almost total isolation doesn't lend itself to manipulating the DOM without the user explicitly writing a custom API to interpret the worker's messages. So, back to the same problem.
I’ve been thinking a little of doing something in this space. But doing so as part of a larger project involving a new programming language so perhaps not applicable to you situation.
I also want to leave the notebook style, focusing more on graph approach. Think higher order spread sheet without the grid.
One, somewhat tangential, thought I’ve had to handle the infinite loop issue though. Is to fragment the language somewhat like they suggest in the out the tarpit paper. Trying to avoid infinite loops by construction, only used at the highest orchestration layer to schedule execution blocks, where the runtime can keep track of things going out of control.
Not sure what it would take to extract a “total” subset of javascript though. But one idea would be to insert a trampoline breaking out of infinite loops often enough for error handling.
To answer some of your questions (let me know if I didn't answer all of them!)
- I think logic errors can't be solved or prevented when exposing vanilla HTML, CSS and JS. You can completely break the notebook if you want to. Like the sibling comment to this stated: the notebook is run in an iframe of a different origin so you (should!) not be able to break out of that iframe. But of course if you introduce some endless loop that crashes the browser (or maybe just the iframe depending on the browser), I can't stop that.
- No particular reason, but it was always a bit iffy getting the return value correctly. To support top level await your code actually gets wrapped into an async function, I don't know how nicely that plays with new Function.
In the future I want to support ES modules with import instead. Perhaps no magic will be required at all anymore and performance will probably be even better (https://2ality.com/2019/10/eval-via-import.html)
- Codemirror is very good and doesn't have a huge bundle size. I decided I wanted to support touchscreens. The monaco editor completely fails on my phone (if I type space it usually copies the word in front of it), whereas codemirror just works. The autocomplete and language support is much worse though, on a desktop I think I will always choose the Monaco editor.
- Probably not the most exiting answer, but for me it was just wanting to use Jupyter for things it was really bad at. There is Project Iodide which is very similar, but they made some different design decisions that move away from the code-output-code-output structure.
By putting the editor inside the sandbox things become much more straightforward and less "special" or "magic", that's another big difference from Project Iodide.
I think more and more fully client-side editors are becoming possible now due to dynamic import and webassembly, do share your project with me if you get the chance to work on it!
Ah, okay this is interesting. The entire notebook is one iframe, which lets you do more of the metaprogramming and sharing variables. Both printing (via console) and non-printing infinite loops crash the iframe until you close the page. And the reason my document.innerHTML = "" test was recoverable is that your notebook-top-button-container is not in the iframe (and it has that refresh button!).
I'll have to look more into async stuff. Great eval article there. I've found these four lines to work for importing content[1]. I dove into realms[2] and the realms shim [3] a while ago after reading about Figma's engineering behind plugins [4] but I never figured out how I might use that for my system.
You might take a look at how the codemirror author built his book [5] and editor [6] in this repo [7]. He implements the loop detection I derided in my first comment. I shouldn't be so harsh, it's a good and simple idea. It's just not an answer, more of a hack. My interests align pretty well with the p5js editor audience[8], so some of the early issues there convey why the simple loop detection isn't ideal.
The only viable way to avoid an endless while loop or overly heavy operations is to have a worker do the eval(), though it would defeat the meta-programming aspect and isolate execution making it pretty much useless. It's still possible to proxy local objects via comlink (or implement your own worker proxy) but it would limit communication to POJO's so no dynamic module imports and access to non-serializable objects.
Right, where you could send the worker a terminate signal if some conditions were met like time or no response or too many responses...
And that drawback obsoletes the strategy it seems, unfortunately.
It's an interesting problem, how to provide full access and control as well as non-corruptible recovery systems.
One of the biggest problems I consistently run into is that printing to the screen is a slow operation, every learns this day one of any graphics work, but things like console.log or Node.appendChild are the primary way to destroy someone's computer for a short period of time because orders of magnitude more operations are queued up before they can complete. I wonder if painting operations could be limited to some max number per second to prevent that orders of magnitude queue problem. And, more importantly, how do you code that limit?
> One of the biggest problems I consistently run into is that printing to the screen is a slow operation, every learns this day one of any graphics work, but things like console.log or Node.appendChild are the primary way to destroy someone's computer for a short period of time because orders of magnitude more operations are queued up before they can complete. I wonder if painting operations could be limited to some max number per second to prevent that orders of magnitude queue problem. And, more importantly, how do you code that limit?
For node.appendChild you have the whole virtual DOM thing leveraged by modern JS frameworks. If you hit issues try using any framework that boasts having a virtual DOM.
The same strategy could be done for the console to some extent.
The built-in console object is over-writable so you can add a layer that would batch console calls (if you do many in a short time). Note that this will break the call address you have on the right-hand side. Also IE11 does strange things with the console object depending if devtools are open or not.
The solution above is doable but it's a hack. If you really have to call console a lot of times in a short time adding a custom log method would be better.
globalThis.__debugLog = [];
globalThis.__printLog = () => globalThis.__debugLog.forEach(i => console.log(i));
const log = (...args) => globalThis.__debugLog.push(args);
// then in code:
log(whatever);
// and once you want to check it, call printLog in the console
__printLog();
Again with this solution you don't know where the call happens but it's a starting point. I recently started using the pattern above to debug performance and memoization issues I had in heavily used code. The console wasn't readable anymore without it. Console.table is helpful for printing out bigger sets of debug data btw.
I meant to dig more into requestAnimationFrame, thanks for the prompt. I should clarify, the problem is that you can crash a web page if you do any animation inside of a runaway loop but especially painting with console. I suppose this indirection might help you realize that before it happens though, and that's probably worth doing. Thanks for the code.
You're welcome. I avoid runaway loops like fire because the crashes tend to be problematic to debug. Consider the timeout thing from SO. Generally working with events in JS is easier than central run loops.
Note that it does not mention promises and async/await but it does include it. Promises are essentially syntactic sugar for a setTimeout calling success and failure methods. This also means that promise code will be executed in parallel but on one thread and one heap, so each promise will fight with all other for priority. This is generally fine for async stuff like fetching resources but it would completely destroy performance if it would do anything heavy, like, say unzipping zips with jszip.
Historically the event-driven model of JS enabled Node.js's API to be callback-driven. This lead to what is called "callback hell" and eventually to a proper introduction of promises and async/await to the language.
A good read, mdn is far and away the best resource I have found for web technologies. I'm excitedly looking into some p2p tech and the webrtc articles are useful.
From the article:
> "Run to completion"...
> A downside of this model is that if a message takes too long to complete, the web application is unable to process user interactions like click or scroll. The browser mitigates this with the "a script is taking too long to run" dialog. A good practice to follow is to make message processing short and if possible cut down one message into several messages.
Good advice. And this is what happens when you `while(1) {console.log("loop")}`. Though I've never see a "taking too long to run" dialog, I'm just eventually able to kill the process myself and sometimes it takes a few minutes while my fans spin.
I've been working on something similar on and off for a while, and I've found the space to be fascinating and somewhat puzzling. Most of the first questions I try to ask about these in browser notebooks don't have clean answers. In no particular order:
- Do you have any ideas about solving logic errors? As it stands right now a while(true) loop crashes the page and consecutive re-openings of the page for a short while (in chrome). What about things like document.body.innerHTML = "" (you actually seem to handle this pretty gracefully, I'll see if I can poke more holes in this another time). For what it's worth none of the online notebooks I've seen have a satisfactory solution to infinite loops, loop timeouts are too blunt an instrument and crashing the tab can take the browser minutes to recover.
- Is there a particular reason you use eval rather than the new Function constructor? From what I've read using new Function is much more performant, and other than different scoping a better choice than eval. Can't find the link at the moment, but it was wrapped up in the mdn[1] design docs for their codebox examples.
- I see you're using the first codemirror 6[2] beta release, how are you liking it? I really enjoy the interface so far.
- Do you have any favorite resources or inspirations about why you went about building a hackable offline local first notebook environment? I particularly like the experiments at Ink and Switch[3]. (many related hn submissions). As well as webstrates[4].
My attempt at this game is to break out of the notebook style single column layout and embrace an art board style canvas, which is a rather radical idea in that it is not obvious what that should look like or how basic interactions like hierarchies or execution order should look, but fun to explore. I also desperately want to prevent crashing as a result of logic errors and workflow footguns(like deleting DOM elements or overwriting storage), to that end I have a separate storage of scripts to rebuild a "safe boot" interface, but there is more thinking to be done here.
I look forward to poking around your code some more soon. Thanks for posting.
[1]: https://developer.mozilla.org/en-US/ [2]: https://codemirror.net/6/ [3]: https://www.inkandswitch.com/ [4]: https://www.webstrates.net/
Edited out a question about your motivations. I reread your comment and the about page and I realized my motivations are similar to yours. While observable doesn't have a few of your key points, it is a fantastic product. The reason I don't settle on it is that I'm interested in experimenting with the environment outside of a notebook-with-cells interface.