Client-server roundtrips during diagram editing

i have diagrams where the some parts of the business logic can only be performed on the server side.
therefore, editing cannot be fully solved on the client-side.

example of drawing new links:

  • i have a diagram based on a standard GraphLinksModel
  • i draw a new link between two nodes using the built-in LinkingTool
  • during the drag and drop operation, i’m perfectly satisfied with the default client side behavior of LinkingTool

but once the LinkingTool drag and drop operation completes, i don’t want to rely on GoJS adding a new link data object to the linkDataArray of the model. at this point i’d rather like to:

  • catch the information of the newly drawn link
  • perform an async client-server roundtrip (client sends info of the new link to the server)
  • receive the resulting graph model data (nodes and links) from the server
  • re-bind GoJS to the newly received model data (there is a possibility that changes other than the creation of the new link will be received from the server)

similarly, i might need to do such client-server roundtrips during other editing events such as deleting nodes and links, moving nodes, applying a new layout, etc.

i’m interested which approach would be considered as the best practice for such scenarios, at which point of the GoJS API the necessary server-client roundtrips should be weaved into the cycle of editing events.

(note: i’m not using the undoManager as it is not necessary to make the edits undoable in my case)

If you want the state on the server to be the primary source of truth, you need to deal with the possibly long delay between the user’s actions and getting back any confirmation and updates from the server. But I think you want to adopt the optimistic model of transactions, where what the user does proceeds normally. You don’t want to do nothing until the server has responded! But what the user does locally may need to be updated with additional information from the server.

You just need to send any user-driven changes to the server and then have the server send back the “real” changes. Those “real” changes you would merge into the diagram’s model within a transaction. That does mean that there could be a noticeable delay between changes that the user makes interactively and adjustments according to what the server decides is “real”, but hopefully you can make such adjustments not be disconcerting to the user.

you have described quite precisely what i want to achieve, and i’m trying to decide which way to go with:

  1. optimistic (client displays an estimated outcome until server returns the full resulting status)
  2. wait for it (freeze -> show spinner, wait for response -> response -> update model -> continue)

as both of the approaches have the pros and cons, i’d like to actually implement both of them, in order to try and see which one will work better for us on the long run.

can you give me an advice regarding which points of the GoJS APIs i should wire my interception code depending on whether i’m implementing the 1st or the 2nd approach?

for now the scope of expected changes from the user is the following:

  • draw new links
  • move nodes, apply automatic layout (like digraph or force directed)
  • delete selection (might be a single node or a subset of the graph)

Very few developers decide for choice #2. It really isn’t friendly to the user. And depending on how much you really want to determine on the server, it could be a lot more work to do there, since you would have to duplicate on the server what is being done and what is easily configured in the diagram in the client.

In general one updates the server at the end of each transaction. https://gojs.net/latest/intro/changedEvents.html#SavingModelWhenTransactionsComplete

i see your point, so basically the recommended best practice would look like this:

  • user does something
  • GoJS does its job, completes transaction, a temporary status is displayed
  • my client code in ModelChangedListener reacts, performs async call to server
  • my server code calculates, sends model in response
  • my client code updates model
  • GoJS updates view

i did some tests wiring my code to ModelChangedListener and monitoring evt.isTransactionFinished.
i’ve got the events i need, except when user applies a new automatic layout.
in this case i’d need the following:

  • user requests layout
  • GoJS calculates new layout
  • GoJS plays animation, and finally displays the new layout

in this scenario i’d like my async call to be triggered once the animation is complete.
in ModelChangedListener i did not get such event, only position changes one by one.

is there any hook, i could use to trigger my code when the animation of the new layout completed?

Actually, all of the nodes have new locations (and links new routes) before any layout animation starts. That state is not drawn if there will be an animation.

Checking for ChangedEvent.isTransactionFinished should be good enough for you. In this case you don’t want or need the “AnimationStarting” and “AnimationFinished” DiagramEvents.

dear Walter, thank you very much for your assistance. based on your advices i managed to implement all the necessary client-server roundtrips. i’m summarizing my solution here:

i adopted the optimistic strategy: client displays an estimated outcome until server returns the full resulting status, then i update the client view.

client-server calls are implemented in the addModelChangedListener hook, monitoring evt.isTransactionFinished flag as described here:

in order to make transactions work, i had to enable the undo manager, but as i don’t actually need the undo functionality, i have disabled the function on the diagram:

this.goDiagram.undoManager.isEnabled = true;
this.goDiagram.allowUndo = false;

in addModelChangedListener, i’m performing my server-client async calls when detecting the following transactions:

      this.goDiagram.addModelChangedListener((evt: go.ChangedEvent) => {
         if (!evt.isTransactionFinished) return;
         var txn = evt.object as go.Transaction;  // a Transaction
         if (txn === null) return;

         if (txn.name == "Linking") {/*async call to server*/}
         else if (txn.name == "Delete") {/*async call to server*/}
         else if (txn.name == "Move") {/*async call to server*/}
         ...
      }

in case i need, i can analyze the underlying changes using:

         txn.changes.each((change: go.ChangedEvent) => {
            if (change.modelChange) {/*analyze underlying change*/}
         });

in order to be able to react on the event when the user applies a new layout (digraph or force directed), i have wrapped the code that sets the new layout in a transaction:

      this.goDiagram.startTransaction("SetLayout");
      this.goDiagram.layout = /*new layout to be applied*/;
      this.goDiagram.commitTransaction("SetLayout");

after doing that i could also weave my client-server call into addModelChangedListener:

      this.goDiagram.addModelChangedListener((evt: go.ChangedEvent) => {
         if (!evt.isTransactionFinished) return;
         var txn = evt.object as go.Transaction;  // a Transaction
         if (txn === null) return;
         ...
         if (txn.name == "SetLayout") {/*async call to server*/}
         ...
      }

i’m satisfied with the resulting behavior.
but please, let me know if you see any flaws or possible improvements to the above.