Transaction Model

Canvas is a scripting language to manipulate documents (templates). It is not only intended to use as a internal model to be able to render things in the browser; you can use it to load documents from the server, modify them and send them back with out showing in the browser nothing more than a progress bar. You can do batch updating of things in the DB like this without the need of asking the server Core team to do it for you. All the power of the engine is there for you to use it in your local machine.

Things can fail

Because of the nature of the interaction between CxEngine in the server and Canvas in the client, some operations are async and there are more places where failures can appear: the internet may be down, the server may be overloaded, etc. Even when you do simple scripting it is useful to be able to work using transactions. Being able to mark valid points in your work allows for a more fine grained treatment of failures. You do not need to redo everything, it is enough to go back til the last valid committed state and do the last part that has failed. In this context, transactions are an important piece of CIL design and are not related to the need of a History feature for the user. You can use CIL without doing any transaction, nobody will force you to do it, transactions are a natural part of the design. A good analogy is to think that you are working with something similar to a revision control system (an old one where you need to explicitly lock resources. So, if you need to modify a figure you will do it like:

figure.lock();
... load resources, modify the figure ...
figure.commit$();

The first lock let others know that this figure is being working on by throwing a document event. Our system allows you to work with figures in parallel, but you can not modify the same figure in parallel.
If you modify the whole document, you will write:

doc.lock();
... modify the doc ...
doc.commit$();

Modifying CIL objects is like changing files in your local repository in the revision system. Your local files (model) are changed until you reach an atomic change set. When you reach that point, you call the .commit$() function to complete the transaction.

If something goes wrong in the middle, you can easily revert your local changes back to the last committed state like:

doc.lock();
... modify the doc ...
... something went wrong ...
doc.revert$();

An important thing to understand here is that the lock/commit$ mechanism is just a transaction scheme and does not generate by itself any history, there is no redo/undo in the picture yet. The mechanism just saves the last committed state to be able to go back to it when you do a revert, so you can program against a failure prone system (like it is the internet, server infrastructure, etc) with better tools. So til here, there is no history, no rendering, no tools disabling, no user feedback. The above description is about our template scripting language.

Canvas

Lets put the Canvas component in the table. This is the main piece used to build browser applications that needs to render templates or interact with user. Now, on top of being able to modify a document, we need to do more things to give a good user experience. The minimal set of things to take care of are:

  • (a) Render the document at moment where it makes sense (avoid showing the guts of batch operations to the user… no jumps around, no flickering because you first loaded something of a given color and later you changed it, etc).
  • (b) Disable all other tools when an operation is being performed so there are no race conditions. Again CIL only allows you to modify one figure at a time.
  • (c) Give visual feedback to show that an operation is being performed, and that the user shouldn’t keep hitting the screen to make it “do something”.
  • (d) Maintain a history of all the operations performed so the user can undo/redo.
  • (e) Select newly added figures.

A possible design is to let the developer deal with these issues. He will need to write code like:

0. canvas.disable(); // b
1. canvas.startFeedback(); // c
2. doc.lock();
3. ... modify the doc ...
4. canvas.selectedFigures(newFigures); // e
5. doc.commit$();
6. history.push(doc); // d
7. canvas.stopFeedback(); // c
8. canvas.enable(); // b
9. canvas.render$(); // a

This is unacceptable, imagine if for just moving a figure we will need almost 10 lines of code.

Canvas tackles this problem by defining that is going to show every committed state of the document. This makes sense because the state at the end of every transaction effectively represent the successful completion of an atomic change set. Canvas listens to the document ‘commit’ event and calls render after it avoiding the need of line 9.

The tools will also be disabled when an operation starts (that is, when someone locks the document to start working on a new change set). Once the change is complete after the ‘commit’, we can safely enable the tools back. Line 0 and 8 can be eliminated.

The same is done for user feedback. As soon as somebody starts an operation we show the user feedback (on ‘lock’) and when the operation is complete we just stop the work in progress feedback (on ‘commit’). Line 1 and 7 are automated.

History is defined as every committed state of the document. Because rendering happens at that point, this means that the user will get a history point for every operation he see completed on the canvas. Canvas automatically creates a history point when the ‘commit’ event is issued removing the need for line 6.

Newly added figures can also be automatically selected when the transaction is done. This creates a standard behavior for user created actions that helps making the UI more coherent. We can then remove line 4.

So, the code to make a proper action ends up being:

doc.lock(); // => Trigger 1, 2
... modify the doc ...
doc.commit$(); // => Trigger 4, 6, 7, 8, 9

The user can just code its action in the same way that he will do to create batch scripts. There is nothing new to learn. Canvas will automatically work.

It makes even more sense when possible failures are analyzed. Every time you call revert$(), Canvas will also be re-enabled and the work in progress feedback will be stopped. No rendering and history needs to be generated here.

An important implication is that clients have a clear pattern to follow to create pre or post-operation events. The client may have other tools to disable/enable that are not part of Canvas. They can listen to the ‘lock’ and ‘commit’ events in the same way that Canvas tools do. Other kind of feedback may be wanted (a progress bar, a loading symbol, a timer). Also, property watchers like a display for the document bounds, or a label showing the current price for the design can no be easily automated to be updated after each commit.

Further details on the lock/commit$ pattern

Conceptually .lock() and .commit() are not really a pair, although they will usually use together. Locking has to do with a limitation of our system, we can no modify the same figure at the same time by two different actions. When you start an action, you have to lock your target (doc or figures). But if your action starts with a modification, you can omit the .lock() call because each modifier will auto-lock like:

figure.pin( ... ); // => auto-lock
figure.commit$(); // => auto-unlock

If you need to load resources before modifying the document in your action you need to be explicit and lock the document:

doc.lock();
... load resources, modify doc ...
doc.commit$();

Our system works like Git in the sense that a commit only modifies the local copy and not the saved document in the server. If you think about the pattern of loading a document, performing some modification and the save it back, then our system will map to Git as follow:

Cx.Document.load$(id) // Git 'checkout'
.then(function(doc){
... modify doc ...
doc.commit$() // Git 'commit'
.then(function(){
... modify doc ...
doc.commit$() // Git 'commit'
.then(function(){
... modify doc ...
doc.commit$() // Git 'commit'
.then(function(){
doc.save(); // Git 'push'
}); }); }); });