From 20dbec24a8aac9f3d6de791bf59fa82b2edbbe6b Mon Sep 17 00:00:00 2001 From: Harry Yu Date: Mon, 4 Oct 2021 15:17:17 -0700 Subject: [PATCH] Fall back to doing a _hardRollback when `op.type.invert` fails --- lib/client/doc.js | 8 +++++++- package.json | 1 + test/client/doc.js | 40 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index e37023b2c..4ce569619 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -948,7 +948,13 @@ Doc.prototype._rollback = function(err) { var op = this.inflightOp; if ('op' in op && op.type.invert) { - op.op = op.type.invert(op.op); + try { + op.op = op.type.invert(op.op); + } catch (error) { + // If the op doesn't support `.invert()`, we just reload the doc + // instead of trying to locally revert it. + return this._hardRollback(err); + } // Transform the undo operation by any pending ops. for (var i = 0; i < this.pendingOps.length; i++) { diff --git a/package.json b/package.json index 09ecc18f3..bf21efa1f 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "nyc": "^14.1.1", "ot-json0-v2": "ottypes/json0", "ot-json1": "^0.3.0", + "rich-text": "^4.1.0", "sharedb-legacy": "npm:sharedb@=1.1.0", "sinon": "^7.5.0" }, diff --git a/test/client/doc.js b/test/client/doc.js index 84af1fb68..eda7592db 100644 --- a/test/client/doc.js +++ b/test/client/doc.js @@ -1,6 +1,9 @@ var Backend = require('../../lib/backend'); var expect = require('chai').expect; var async = require('async'); +var json0 = require('ot-json0').type; +var richText = require('rich-text').type; +var ShareDBError = require('../../lib/error'); var errorHandler = require('../util').errorHandler; describe('Doc', function() { @@ -407,6 +410,43 @@ describe('Doc', function() { ], done); }); + it('rolls the doc back even if the op is not invertible', function(done) { + var backend = this.backend; + + async.series([ + function(next) { + // Register the rich text type, which can't be inverted + json0.registerSubtype(richText); + + var validOp = {p: ['richName'], oi: {ops: [{insert: 'Scooby\n'}]}}; + doc.submitOp(validOp, function(error) { + expect(error).to.not.exist; + next(); + }); + }, + function(next) { + // Make the server reject this insertion + backend.use('submit', function(_context, backendNext) { + backendNext(new ShareDBError(ShareDBError.CODES.ERR_UNKNOWN_ERROR, 'Custom unknown error')); + }); + var nonInvertibleOp = {p: ['richName'], t: 'rich-text', o: [{insert: 'e'}]}; + + // The server error should get all the way back to our handler + doc.submitOp(nonInvertibleOp, function(error) { + expect(error.message).to.eql('Custom unknown error'); + next(); + }); + }, + doc.whenNothingPending.bind(doc), + function(next) { + // The doc should have been reverted successfully + expect(doc.data).to.eql({name: 'Scooby', richName: {ops: [{insert: 'Scooby\n'}]}}); + next(); + } + ], done); + }); + + it('rescues an irreversible op collision', function(done) { // This test case attempts to reconstruct the following corner case, with // two independent references to the same document. We submit two simultaneous, but