diff --git a/src/autosave.js b/src/autosave.js index 4dc28b3..cc1fe44 100644 --- a/src/autosave.js +++ b/src/autosave.js @@ -88,6 +88,8 @@ export default class Autosave extends Plugin { * * synchronized – When all changes are saved. * * waiting – When the plugin is waiting for other changes before calling `adapter#save()` and `config.autosave.save()`. * * saving – When the provided save method is called and the plugin waits for the response. + * * error &ndash When the provided save method will throw an error. This state immediately changes to the `saving` state and + * the save method will be called again in the short period of time. * * @member {'synchronized'|'waiting'|'saving'} #state */ @@ -237,6 +239,18 @@ export default class Autosave extends Plugin { .then( () => Promise.all( this._saveCallbacks.map( cb => cb( this.editor ) ) ) ) + // In case of an error re-try the save later and throw the original error. + // Being in the `saving` state ensures that the debounced save action + // won't be delayed further by the `change:data` event listener. + .catch( err => { + this.state = 'error'; + // Change immediately to the `saving` state so the `change:state` event will be fired. + this.state = 'saving'; + + this._debouncedSave(); + + throw err; + } ) .then( () => { if ( this.editor.model.document.version > this._lastDocumentVersion ) { this.state = 'waiting'; diff --git a/tests/autosave.js b/tests/autosave.js index 643a176..214e03c 100644 --- a/tests/autosave.js +++ b/tests/autosave.js @@ -617,13 +617,52 @@ describe( 'Autosave', () => { expect( pendingActions.hasAny ).to.be.true; sinon.clock.tick( 1000 ); } ) - .then( () => Promise.resolve() ) + .then( runPromiseCycles ) .then( () => { expect( pendingActions.hasAny ).to.be.false; sinon.assert.calledOnce( serverActionSpy ); sinon.assert.calledOnce( serverActionStub ); } ); } ); + + it( 'should handle a situration when the save callback throws an error', () => { + const pendingActions = editor.plugins.get( PendingActions ); + const successServerActionSpy = sinon.spy(); + const serverActionStub = sinon.stub(); + + serverActionStub.onFirstCall() + .rejects( new Error( 'foo' ) ); + + serverActionStub.onSecondCall() + .callsFake( successServerActionSpy ); + + autosave.adapter = { + save: serverActionStub + }; + + editor.model.change( writer => { + writer.setSelection( writer.createRangeIn( editor.model.document.getRoot().getChild( 0 ) ) ); + editor.model.insertContent( writer.createText( 'foo' ) ); + } ); + + return editor.destroy() + .then( () => { + expect( pendingActions.hasAny ).to.be.true; + } ) + .then( runPromiseCycles ) + .then( () => { + expect( pendingActions.hasAny ).to.be.true; + sinon.assert.calledOnce( serverActionStub ); + sinon.assert.notCalled( successServerActionSpy ); + } ) + .then( () => sinon.clock.tick( 1000 ) ) + .then( runPromiseCycles ) + .then( () => { + expect( pendingActions.hasAny ).to.be.false; + sinon.assert.calledTwice( serverActionStub ); + sinon.assert.calledOnce( successServerActionSpy ); + } ); + } ); } ); it( 'should run callbacks until the editor is in the ready state', () => {