|
52 | 52 | unsavedChapters = false, |
53 | 53 | unsavedLayout = false, |
54 | 54 |
|
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 = []; |
56 | 62 |
|
57 | 63 | // Long enough that a burst of edits coalesces into one write, short enough |
58 | 64 | // that a collaborator's refresh shows recent work. |
|
184 | 190 | // module no-ops when there is no server or nobody is logged in, and |
185 | 191 | // re-targets itself when the hypervideo changes. |
186 | 192 | 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 = []; |
187 | 197 | if (FrameTrail.module('Collaboration')) { |
188 | 198 | FrameTrail.module('Collaboration').start('hypervideo', FrameTrail.module('RouteNavigation').hypervideoID); |
189 | 199 | } |
|
1091 | 1101 | } |
1092 | 1102 |
|
1093 | 1103 |
|
| 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 | + |
1094 | 1144 | /** |
1095 | 1145 | * I am the central function for saving changes back to the server. |
1096 | 1146 | * |
|
1110 | 1160 | */ |
1111 | 1161 | function save(callback, callbackCancel, silent) { |
1112 | 1162 |
|
| 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 | + |
1113 | 1187 | var saveRequests = [], |
1114 | 1188 | callbackReturns = [], |
1115 | 1189 | databaseCallback = function(result) { |
|
1153 | 1227 | }, |
1154 | 1228 |
|
1155 | 1229 | function(){ |
| 1230 | + releaseSave(false); |
1156 | 1231 | if (callbackCancel) { |
1157 | 1232 | callbackCancel.call(); |
1158 | 1233 | } |
|
1180 | 1255 | } else { |
1181 | 1256 | showSaveConflictDialog(result.conflict); |
1182 | 1257 | } |
| 1258 | + releaseSave(false); |
1183 | 1259 | return; |
1184 | 1260 | } |
1185 | 1261 |
|
1186 | 1262 | if (result.failed) { |
1187 | 1263 | if (!silent) { |
1188 | 1264 | FrameTrail.module('InterfaceModal').showErrorMessage(labels['ErrorSavingData'] +' ('+ result.error +': '+ result.code +')'); |
1189 | 1265 | } |
| 1266 | + releaseSave(false); |
1190 | 1267 | return; |
1191 | 1268 | } |
1192 | 1269 |
|
|
1224 | 1301 | action: 'EditSave' |
1225 | 1302 | }); |
1226 | 1303 |
|
| 1304 | + releaseSave(true); |
| 1305 | + |
1227 | 1306 | if (callback) { |
1228 | 1307 | callback.call(); |
1229 | 1308 | } |
|
0 commit comments