Concurrent Saves

Sometimes you need to be able to make changes and save them while a previous save is in progress or "in flight" as we say. This second save is concurrent with the first. A single EntityManager will refuse to perform a concurrent save but you can work around it with techniques and tools described in this topic. You'll learn about

Save is asynchronous

A client-side save is an asynchronous operation. It can take a while before the server replies with a report of success or failure.

Until then, entities remain in cache in their pending-save-states (that is, their EntityStates remain "Added", "Modified", and "Deleted"). Added entities with store-generated keys still have their temporary values. Only when the server reports success will Breeze (a) replace temporary key values with permanent key values and (b) change the EntityStates to "Unchanged". If the server reports a failure, the entities remain in their changed states with temporary key values and you, the developer, must figure out what to do next.

For these reasons, we recommend blocking further changes until the save succeeds or fails. You should not try to manipulate these pending entities until the save returns. You must leave them in their present, changed state until the pending save resolves. Postpone your efforts until the success callback.

manager.saveChanges()
             .then(success)
             .catch(fail);

function success(saveResult) {
   /* do your post-save work here */
}

No concurrent saves by default

You can ignore our recommendation. Nothing in Breeze prevents the user from making more changes while the save is still in progress. The user could even make changes to the entities that are "in flight". That is probably not a good idea but it's up to you to determine how to handle this situation. You're dealing with more of a user experience issue than a technical issue.

While you may let the user makes changes at their peril, know this:  the Breeze EntityManager will throw an exception if you ask it to save again while a prior save is "in flight"!

It's too risky to let you do that without your express permission. For example, if you are saving a new entity and its key is store-generated, a second save will cause that entity to be saved a second time and you will get duplicate records in your database. There is no known solution to this problem except to block the second save until the first completes.

There is a similar problem with pending deletes; the second save tries to delete the entity again ... which is no longer in the database and likely triggers a concurrency violation.

Maybe you know what you are doing. Maybe the user can modify entities but can't add or delete them on this screen. Well you can tell Breeze to allow concurrent saves by creating a manager with the allowConcurrentSaves option.

var manager = new breeze.EntityManager({
        dataservice: "api/Todo",
        saveOptions: new breeze.SaveOptions({allowConcurrentSaves: true})
    });

Now the potential for trouble is yours to manage.

Sandbox editors

We suggest that you block the UI while waiting for the save to complete. That's not too bad if you have a fast connection. Of course it's an unpleasant experience if the save takes more than a second ... as is likely in mobile scenarios.

In that case, it may be possible to isolate activity that could change entities in a  "sandbox" editor with its own manager. The editor is a screen devoted to changing "one thing". The user will understand if you make her wait while these changes are saved.

Meanwhile, she can switch to other screens and get work done. Perhaps she can review a list of things to do, pick one and launch another sandbox editor for that task. You won't have to worry about the pending save. The "entities in flight" are in the original sandbox manager and are immune to changes in other managers backing other screens.

Examples and discussion of this approach are forthcoming. In the meanwhile, see Ward's answer to this pertinent StackOverflow question.

Queuing Saves

Maybe your required workflow won't permit the editing of "one thing" at a time on its own screen. For example, you might want to save each entry in an expense report. You don't want to add or edit every expense item in its own screen. You want to edit them quickly, perhaps in a grid or list layout. You need another approach besides the sandbox editor.

It may be feasible to allow adds and edits on the same screen using the same manager without blocking user input between saves. The trick is to queue subsequent save requests while the first save is in progress.

The Breeze EntityManager.saveChanges method does not support "save queueing" natively. Fortunately, there is a Breeze plugin that will add this capability and that plugin is lurking among the samples. You'll find it in the NoDb sample, in its Scripts folder, as "breeze.savequeuing.js".

This same breeze.savequeuing.js plugin is included in the Breeze MVC SPA Template which will go live soon.

The breeze.savequeuing.js implementation is too lengthy to reproduce here. The idea is pretty simple.

When you call manager.saveChanges() and there are no pending saves, it processes the save immediately and returns a promise.

On the other hand, if another save is in progress,the manager puts the request on an internal save queue. That queued request is a promise. The manager returns that promise just as it returned a promise for the first save request. The caller can't tell the difference. It was going to wait for the save to complete anyway; it will just have to wait a little longer for this one.

When the first save returns successfully, the manager pops the next request off the save queue and processes it. The saga continues with more requests added to the queue and more removed until the queue runs dry. Save requests are processed in "first in, first out" order.

A save failure at any point usually leaves the manager's cache in an uncertain state with the potential for making a mess of the remaining queued saves. Therefore, If a save request fails, the manager stops trying to save the remaining queued saves. Instead it will pass each of them a failure message. Remember that each request is a promise, so the callers who placed those save requests will receive that failure message in their fail callbacks.

To use "save queuing" do the following:

  1. Acquire the breeze.savequeuing.js plugin (e.g., from Github).
  2. Load it in a script tag just after the BreezeJS script tag.
  3. Enable "save queuing" on the manager.
  4. Call manager.saveChanges() as before.

Here's an example of the script tags

<script src="Scripts/q.js"></script>
<script src="Scripts/breeze.debug.js"></script>
<script src="Scripts/breeze.savequeuing.js"></script>
        
<!-- App libraries -->
<script src="Scripts/app/todo.bindings.js"></script>
 ... more app scripts ...

The breeze.savequeuing.js plugin adds an enableSaveQueuing method to the EntityManager class.

manager.enableSaveQueuing(true);

When true, the plugin replace the native saveChanges method with a queuing wrapper around that method. If you disable queuing, the plugin restores the original saveChanges method.

Here's your save call ... showing no signs of the queuing within

return manager.saveChanges();

The caller should append the appropriate then(saveSuccess) and catch(saveFailure) callbacks to the returned promise.

Time-delayed saves

You may have seen an alternative "time-delayed" approach to preventing concurrent saves. Here's an excerpt from the dataservice.js in the Todo sample

 function saveChanges(...) {
    ...
    if (_isSaving) {
         setTimeout(saveChanges, 50);
         return;
    }
    _isSaving = true;
    manager.saveChanges()
           .then(saveSucceeded)
           .catch(saveFailed)
           .finally(saveFinished);

    ... implementations of saveSucceeded, saveFailed, saveFinished
}

If a save is pending (_isSaving ===  true), the method waits 50 milliseconds and tries again. Simple in concept and it takes much less code to implement than the plugin.

It's not a good solution to the problem for several reasons:

  • it doesn't return a promise so the caller can't do something when the first or subsequent save succeeds or fails
  • the code becomes vastly more complicated if you try to make it return a promise
  • the saveChanges implementation is kind of a mess compared to the equivalent in "save queuing".
  • the 50 milliseconds is an arbitrary guess that may be too long or too short
  • the original sequence of save requests is not guaranteed; the actual order is an accident of timing
  • it processes the remaining saves after the first one fails.

It's perfectly adequate for the "Todo" demo. Don't use it in your code; use "save queuing" instead.