FIX: never save draft while it is saving

FIX: never save draft while it is saving

Previously if saving a draft took longer than 2 seconds there could be conditions where drafts could be saved concurrently. This meant the composer could race with itself and raise conflicts.

This is likely to happen on bad internet connections or where latency is really high.

Additionally a throttle was added so drafts save unconditionally every 15 seconds.

Save draft in the model now properly and consistently returns a promise.

diff --git a/app/assets/javascripts/discourse/app/controllers/composer.js b/app/assets/javascripts/discourse/app/controllers/composer.js
index c589c7f..e86b14b 100644
--- a/app/assets/javascripts/discourse/app/controllers/composer.js
+++ b/app/assets/javascripts/discourse/app/controllers/composer.js
@@ -1095,7 +1095,13 @@ export default Controller.extend({
   _saveDraft() {
     const model = this.model;
     if (model) {
-      model.saveDraft();
+      if (model.draftSaving) {
+        debounce(this, this._saveDraft, 2000);
+      } else {
+        model.saveDraft().finally(() => {
+          this._lastDraftSaved = Date.now();
+        });
+      }
     }
   },
 
@@ -1107,7 +1113,15 @@ export default Controller.extend({
       !this.skipAutoSave &&
       !this.model.disableDrafts
     ) {
-      debounce(this, this._saveDraft, 2000);
+      if (!this._lastDraftSaved) {
+        // pretend so we get a save unconditionally in 15 secs
+        this._lastDraftSaved = Date.now();
+      }
+      if (Date.now() - this._lastDraftSaved > 15000) {
+        this._saveDraft();
+      } else {
+        debounce(this, this._saveDraft, 2000);
+      }
     }
   },
 
diff --git a/app/assets/javascripts/discourse/app/models/composer.js b/app/assets/javascripts/discourse/app/models/composer.js
index 4f87162..a17461a 100644
--- a/app/assets/javascripts/discourse/app/models/composer.js
+++ b/app/assets/javascripts/discourse/app/models/composer.js
@@ -1086,12 +1086,14 @@ const Composer = RestModel.extend({
   },
 
   saveDraft() {
+    if (this.draftSaving) return Promise.resolve();
+
     // Do not save when drafts are disabled
-    if (this.disableDrafts) return;
+    if (this.disableDrafts) return Promise.resolve();
 
     if (this.canEditTitle) {
       // Save title and/or post body
-      if (!this.title && !this.reply) return;
+      if (!this.title && !this.reply) return Promise.resolve();
 
       if (
         this.title &&
@@ -1099,14 +1101,15 @@ const Composer = RestModel.extend({
         this.reply &&
         this.replyLength < this.siteSettings.min_post_length
       ) {
-        return;
+        return Promise.resolve();
       }
     } else {
       // Do not save when there is no reply
-      if (!this.reply) return;
+      if (!this.reply) return Promise.resolve();
 
       // Do not save when the reply's length is too small
-      if (this.replyLength < this.siteSettings.min_post_length) return;
+      if (this.replyLength < this.siteSettings.min_post_length)
+        return Promise.resolve();
     }
 
     this.setProperties({
@@ -1138,13 +1141,11 @@ const Composer = RestModel.extend({
         }
         if (result.conflict_user) {
           this.setProperties({
-            draftSaving: false,
             draftStatus: I18n.t("composer.edit_conflict"),
             draftConflictUser: result.conflict_user
           });
         } else {
           this.setProperties({
-            draftSaving: false,
             draftSaved: true,
             draftConflictUser: null
           });
@@ -1169,10 +1170,12 @@ const Composer = RestModel.extend({
         }
 
         this.setProperties({
-          draftSaving: false,
           draftStatus: draftStatus || I18n.t("composer.drafts_offline"),
           draftConflictUser: null
         });
+      })
+      .finally(() => {
+        this.set("draftSaving", false);
       });
   },
 

GitHub sha: ce4b5b56

1 Like

This commit has been mentioned on Discourse Meta. There might be relevant details there:

https://meta.discourse.org/t/sth-wrong-with-draft-is-being-edited-in-another-window/151430/6

Great! I love this solution.