The Object Model in Livelymerge
Tenfold: Celebrating 10 Years of Ink & Switch
Introduction
In the Livelymerge project, Dan Ingalls, Peter Van Hardenberg, and I (Alex Warth) are exploring the opportunities and challenges that arise from using an Automerge document as the heap of a program.
A couple of opportunities:
Persistence: The program’s heap is persistent, similar to a Smalltalk image. If you close the program today and open it two weeks from now, every object will be exactly as you left it.
Collaboration: In a multi-user context, all of the participants will share the same heap, which makes this an interesting medium for collaboration.
Some challenges:
Consistency:
When multiple users are present, who is in charge of running what “processes”? E.g., if the program implements a simulation and more than one user is running the code, it’s possible that some side effects will run two or more times which is undesirable.
Is it possible to represent the program’s state in such a way that its invariants will be preserved when automatic merges happen as a result of multiple users’ interactions with overlapping sets of objects?
Performance: Can we get programs to run fast enough for authentic use?
Support for long-running programs: We’d like to be able to support programs that we can “live in” — the kind of system Dan is known for, like Squeak and the Lively Kernel. But these programs run for an unbounded amount of time, and so their corresponding Automerge documents will accumulate a very large number of changes. Can we pull this off? If not (given the current implementation of Automerge) are there changes to AM (planned or otherwise) that could make this work?
This note describes the object model we designed and implemented for this project. It automatically serializes and deserializes data from/to the program’s Automerge document. You’ll see what this means and why it’s needed soon. But first…
Why “Lively”?
The programs we’re most interested in for this project are self-sustaining systems like Squeak and the Lively Kernel. We’re creating a new system in the same vein, but this time we’re designing it from the ground up to be “multi-user” and collaborative, leveraging the good stuff that we get from Automerge.
Dan’s Lively Kernel (LK) is a Squeak-like system that was written entirely in Javascript and runs inside the web browser. A user of the system can conjure up a Smalltalk-style browser and modify any aspect of the system (e.g., the way text editing works or even the browser itself!) while it’s running. The effects of the user’s changes happen immediately.
As part of the Livelymerge project, Dan has written a new LK-like system whose heap is represented as an Automerge document. It includes a graphical user interface based on Morphic, editable text areas, and even a Smalltalk-style browser. Everything in the system is written from scratch (the graphics bottom out at the HTML canvas) and the code can be viewed and edited from inside the system. This means that the user can make fundamental changes to the system, and in a multi-user context, those changes apply to all of the participants.
(Sidebar: for a long time, I’ve wanted my colleagues at Ink & Switch to experience this kind of self-sustaining system firsthand, and this project was a good excuse to make that happen.)
LM’s Object Model
LM programs are written in ordinary JavaScript: object and array literals, top-level declarations, closures, even class syntax all work the way you’d expect. The difference is where the objects live: their state is represented in the program’s associated Automerge document instead of the JS heap. This means that our objects are persistent and support collaboration right out of the box.
The implementation has two main ingredients:
An object model — the subject of this note — consisting of the global object (the equivalent of JavaScript’s globalThis, and the root of the heap) plus a small set of primitives for creating new objects, arrays, and functions in the heap.
A transpiler that rewrites plain JavaScript to use those primitives, so you never call them yourself. (The transpiler probably deserves its own lab note; here I’ll stick to the object model underneath it.)
Here’s a simple example to get us going:
f = (x, y) => x * y + 2;<br>f(5, 8); // evaluates to 42
This looks pretty “vanilla” so far, but there’s something interesting going on. Because f is a global, it lives in the program’s heap — i.e., in the Automerge document. For example, suppose you and I are both working on this program, but from different computers. If I evaluate the first statement, f is stored in our heap. This means that if you evaluate the second statement without having evaluated the first , you will still get the expected result (42).
Here’s another example — this one usually gets an “oooohhh” when we demo it:
makeCounter = () => {<br>let count = 0;<br>return () => ++count;<br>};<br>counter = makeCounter();<br>counter(); // evaluates...