Skip to content

Commit 9495643

Browse files
Avoid save conflicts between manual and auto-save
1 parent c3d6a26 commit 9495643

1 file changed

Lines changed: 80 additions & 1 deletion

File tree

src/player/modules/HypervideoModel/module.js

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,13 @@
5252
unsavedChapters = false,
5353
unsavedLayout = false,
5454

55-
autoSaveTimeout = null;
55+
autoSaveTimeout = null,
56+
57+
// Two writes must never be in flight at once. Both would carry the
58+
// baseVersion loaded from disk, so the second to land would lose the
59+
// server's compare-and-swap and report a conflict — with ourselves.
60+
saveInFlight = false,
61+
pendingSaves = [];
5662

5763
// Long enough that a burst of edits coalesces into one write, short enough
5864
// that a collaborator's refresh shows recent work.
@@ -184,6 +190,10 @@
184190
// module no-ops when there is no server or nobody is logged in, and
185191
// re-targets itself when the hypervideo changes.
186192
window.clearTimeout(autoSaveTimeout);
193+
// A save deferred behind an in-flight one belongs to the model we are
194+
// replacing; flushing it after the swap would report a save of data
195+
// this init has just discarded.
196+
pendingSaves = [];
187197
if (FrameTrail.module('Collaboration')) {
188198
FrameTrail.module('Collaboration').start('hypervideo', FrameTrail.module('RouteNavigation').hypervideoID);
189199
}
@@ -1091,6 +1101,46 @@
10911101
}
10921102

10931103

1104+
/**
1105+
* Hand the write lane back once a save has returned.
1106+
*
1107+
* A user-initiated save that arrived while that one was running was
1108+
* deferred rather than dropped, because it owes the user feedback — so it
1109+
* starts here. By then the unsaved-flags are usually already clean, in
1110+
* which case it completes immediately and just reports success, which is
1111+
* exactly what somebody who pressed Save expects to see.
1112+
*
1113+
* After a failed save the deferred ones are discarded instead: they would
1114+
* run into the same error and stack a second dialog on top of the first.
1115+
*
1116+
* @method releaseSave
1117+
* @param {Boolean} runDeferred
1118+
*/
1119+
function releaseSave(runDeferred) {
1120+
1121+
saveInFlight = false;
1122+
1123+
var deferred = pendingSaves;
1124+
pendingSaves = [];
1125+
1126+
if (!runDeferred || !deferred.length) return;
1127+
1128+
save(
1129+
function() {
1130+
for (var i = 0; i < deferred.length; i++) {
1131+
if (deferred[i].callback) deferred[i].callback.call();
1132+
}
1133+
},
1134+
function() {
1135+
for (var i = 0; i < deferred.length; i++) {
1136+
if (deferred[i].callbackCancel) deferred[i].callbackCancel.call();
1137+
}
1138+
}
1139+
);
1140+
1141+
}
1142+
1143+
10941144
/**
10951145
* I am the central function for saving changes back to the server.
10961146
*
@@ -1110,6 +1160,30 @@
11101160
*/
11111161
function save(callback, callbackCancel, silent) {
11121162

1163+
// A save must never be trailed by the auto-save timer firing into the
1164+
// middle of it. The debounce is armed from the last edit, so ~10s later
1165+
// is precisely when somebody reaches for the Save button.
1166+
window.clearTimeout(autoSaveTimeout);
1167+
1168+
if (saveInFlight) {
1169+
1170+
if (silent) {
1171+
// An automatic save has nothing to add to a write that is
1172+
// already running. Re-arm the debounce instead, so edits made
1173+
// while it was in flight are still picked up afterwards.
1174+
scheduleAutoSave();
1175+
return;
1176+
}
1177+
1178+
// A user-initiated save owes the user an answer, so it is deferred
1179+
// rather than dropped, and runs once the current write returns.
1180+
pendingSaves.push({ callback: callback, callbackCancel: callbackCancel });
1181+
return;
1182+
1183+
}
1184+
1185+
saveInFlight = true;
1186+
11131187
var saveRequests = [],
11141188
callbackReturns = [],
11151189
databaseCallback = function(result) {
@@ -1153,6 +1227,7 @@
11531227
},
11541228

11551229
function(){
1230+
releaseSave(false);
11561231
if (callbackCancel) {
11571232
callbackCancel.call();
11581233
}
@@ -1180,13 +1255,15 @@
11801255
} else {
11811256
showSaveConflictDialog(result.conflict);
11821257
}
1258+
releaseSave(false);
11831259
return;
11841260
}
11851261

11861262
if (result.failed) {
11871263
if (!silent) {
11881264
FrameTrail.module('InterfaceModal').showErrorMessage(labels['ErrorSavingData'] +' ('+ result.error +': '+ result.code +')');
11891265
}
1266+
releaseSave(false);
11901267
return;
11911268
}
11921269

@@ -1224,6 +1301,8 @@
12241301
action: 'EditSave'
12251302
});
12261303

1304+
releaseSave(true);
1305+
12271306
if (callback) {
12281307
callback.call();
12291308
}

0 commit comments

Comments
 (0)