Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement undo/redo #216

Open
wants to merge 30 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
8705c4f
Fix Doc.prototype.destroy
gkubisa Apr 18, 2018
af84be6
Update tested nodejs versions in .travis.yml
gkubisa Apr 18, 2018
09edf92
Add a test
gkubisa Apr 19, 2018
c15448f
Merge 'upstream/master' into fix-doc-destroy
gkubisa Jun 11, 2018
9f843ef
Implement undo/redo
gkubisa Jun 21, 2018
15cdd1d
Update tested nodejs versions
gkubisa Jul 12, 2018
cfca37f
Make destroy wait for unsubscribe
gkubisa Jul 12, 2018
5e009d1
Simplify the code
gkubisa Jul 12, 2018
efd6e6e
Support skipNoop option
gkubisa Jul 16, 2018
d5f0225
Fails fast if type in not invertible
gkubisa Jul 17, 2018
ade98a4
Implement UndoManager (WIP)
gkubisa Jul 18, 2018
f289a58
Simplify the code
gkubisa Jul 18, 2018
498ee3f
Document connection.undoManager(options)
gkubisa Jul 18, 2018
1eb1d3c
Fix some issues
gkubisa Jul 18, 2018
12392a1
Merge remote-tracking branch 'origin/fix-doc-destroy' into undo-and-redo
gkubisa Jul 18, 2018
1f9cb2e
Clear undo/redo stacks on doc destroy
gkubisa Jul 18, 2018
563bc80
Add test for undoManager.destroy()
gkubisa Jul 18, 2018
11ec724
Add more tests
gkubisa Jul 18, 2018
33be4b9
Add a test
gkubisa Jul 18, 2018
14e5180
Update mocha and fix 2 tests
gkubisa Jul 19, 2018
e45dcc9
Fix sharedb does not exist
gkubisa Jul 19, 2018
d5a03f3
Remove unused variable
gkubisa Jul 19, 2018
acf496b
Merge branch 'update-mocha' into undo-and-redo
gkubisa Jul 19, 2018
d09e506
Update dependencies
gkubisa Jul 19, 2018
bfba7c0
Clean up after merge conflict
gkubisa Jul 19, 2018
c7170d7
Transform by undo and redo ops
gkubisa Jul 19, 2018
eb3cea2
Rename a method and option for undo manager
gkubisa Jul 19, 2018
684725a
Update docs
gkubisa Jul 20, 2018
e0787b7
Add more tests
gkubisa Jul 20, 2018
a788f1e
Move some tests
gkubisa Jul 20, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
root = true

[*]
indent_style = space
indent_size = 2
end_of_line = LF
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
4 changes: 1 addition & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
language: node_js
node_js:
- "10"
- "9"
- "8"
- "6"
- "4"
script: "npm run jshint && npm run test-cover"
script: "ln -s .. node_modules/sharedb; npm run jshint && npm run test-cover"
# Send coverage data to Coveralls
after_script: "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js"
48 changes: 46 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ tracker](https://github.com/share/sharedb/issues).

- Realtime synchronization of any JSON document
- Concurrent multi-user collaboration
- Local undo and redo
- Synchronous editing API with asynchronous eventual consistency
- Realtime query subscriptions
- Simple integration with any database - [MongoDB](https://github.com/share/sharedb-mongo), [PostgresQL](https://github.com/share/sharedb-postgres) (experimental)
Expand All @@ -38,7 +39,7 @@ var socket = new WebSocket('ws://' + window.location.host);
var connection = new sharedb.Connection(socket);
```

The native Websocket object that you feed to ShareDB's `Connection` constructor **does not** handle reconnections.
The native Websocket object that you feed to ShareDB's `Connection` constructor **does not** handle reconnections.

The easiest way is to give it a WebSocket object that does reconnect. There are plenty of example on the web. The most important thing is that the custom reconnecting websocket, must have the same API as the native rfc6455 version.

Expand Down Expand Up @@ -228,9 +229,15 @@ changes. Returns a [`ShareDB.Query`](#class-sharedbquery) instance.
* `options.*`
All other options are passed through to the database adapter.

`connection.createUndoManager(options)` creates a new `UndoManager`.

* `options.source` if specified, only the operations from that `source` will be undo-able. If `null` or `undefined`, the `source` filter is disabled.
* `options.limit` the max number of operations to keep on the undo stack.
* `options.composeInterval` the max time difference between operations in milliseconds, which still allows the operations to be composed on the undo stack.

### Class: `ShareDB.Doc`

`doc.type` _(String_)
`doc.type` _(String)_
The [OT type](https://github.com/ottypes/docs) of this document

`doc.id` _(String)_
Expand Down Expand Up @@ -287,6 +294,19 @@ Apply operation to document and send it to the server.
[operations for the default `'ot-json0'` type](https://github.com/ottypes/json0#summary-of-operations).
Call this after you've either fetched or subscribed to the document.
* `options.source` Argument passed to the `'op'` event locally. This is not sent to the server or other clients. Defaults to `true`.
* `options.skipNoop` Should processing be skipped entirely, if `op` is a no-op. Defaults to `false`.
* `options.undoable` Should it be possible to undo this operation. Defaults to `false`.
* `options.fixUp` If true, this operation is meant to fix the current invalid state of the snapshot. It also updates UndoManagers accordingly. This feature requires the OT type to implement `compose`.

`doc.submitSnapshot(snapshot[, options][, function(err) {...}])`
Diff the current and the provided snapshots to generate an operation, apply the operation to the document and send it to the server.
`snapshot` structure depends on the document type.
Call this after you've either fetched or subscribed to the document.
* `options.source` Argument passed to the `'op'` event locally. This is not sent to the server or other clients. Defaults to `true`.
* `options.skipNoop` Should processing be skipped entirely, if `op` is a no-op. Defaults to `false`.
* `options.undoable` Should it be possible to undo this operation. Defaults to `false`.
* `options.fixUp` If true, this operation is meant to fix the current invalid state of the snapshot. It also updates UndoManagers accordingly. This feature requires the OT type to implement `compose`.
* `options.diffHint` A hint passed into the `diff`/`diffX` functions defined by the document type.

`doc.del([options][, function(err) {...}])`
Delete the document locally and send delete operation to the server.
Expand Down Expand Up @@ -338,6 +358,28 @@ after a sequence of diffs are handled.
`query.on('extra', function() {...}))`
(Only fires on subscription queries) `query.extra` changed.

### Class: `ShareDB.UndoManager`

`undoManager.canUndo()`
Return `true`, if there's an operation on the undo stack that can be undone, otherwise `false`.

`undoManager.undo([options][, function(err) {...}])`
Undo a previously applied undoable or redo operation.
* `options.source` Argument passed to the `'op'` event locally. This is not sent to the server or other clients. Defaults to `true`.

`undoManager.canRedo()`
Return `true`, if there's an operation on the redo stack that can be undone, otherwise `false`.

`undoManager.redo([options][, function(err) {...}])`
Redo a previously applied undo operation.
* `options.source` Argument passed to the `'op'` event locally. This is not sent to the server or other clients. Defaults to `true`.

`undoManager.clear(doc)`
Remove operations from the undo and redo stacks.
* `doc` if specified, only the operations on that doc are removed, otherwise all operations are removed.

`undoManager.destroy()`
Remove all operations from the undo and redo stacks, and stop recording new operations.

## Error codes

Expand Down Expand Up @@ -376,6 +418,8 @@ Additional fields may be added to the error object for debugging context dependi
* 4021 - Invalid client id
* 4022 - Database adapter does not support queries
* 4023 - Cannot project snapshots of this type
* 4024 - OT Type does not support `diff` nor `diffX`
* 4025 - OT Type does not support `invert` nor `applyAndInvert`

### 5000 - Internal error

Expand Down
47 changes: 47 additions & 0 deletions lib/client/connection.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
var Doc = require('./doc');
var Query = require('./query');
var UndoManager = require('./undoManager');
var emitter = require('../emitter');
var ShareDBError = require('../error');
var types = require('../types');
Expand Down Expand Up @@ -33,6 +34,9 @@ function Connection(socket) {
// (created documents MUST BE UNIQUE)
this.collections = {};

// A list of active UndoManagers.
this.undoManagers = [];

// Each query is created with an id that the server uses when it sends us
// info about the query (updates, etc)
this.nextQueryId = 1;
Expand Down Expand Up @@ -584,3 +588,46 @@ Connection.prototype._firstQuery = function(fn) {
}
}
};

Connection.prototype.createUndoManager = function(options) {
var undoManager = new UndoManager(this, options);
this.undoManagers.push(undoManager);
return undoManager;
};

Connection.prototype.removeUndoManager = function(undoManager) {
var index = this.undoManagers.indexOf(undoManager);
if (index >= 0) {
this.undoManagers.splice(index, 1);
}
};

Connection.prototype.onDocLoad = function(doc) {
for (var i = 0; i < this.undoManagers.length; i++) {
this.undoManagers[i].onDocLoad(doc);
}
};

Connection.prototype.onDocDestroy = function(doc) {
for (var i = 0; i < this.undoManagers.length; i++) {
this.undoManagers[i].onDocDestroy(doc);
}
};

Connection.prototype.onDocCreate = function(doc) {
for (var i = 0; i < this.undoManagers.length; i++) {
this.undoManagers[i].onDocCreate(doc);
}
};

Connection.prototype.onDocDelete = function(doc) {
for (var i = 0; i < this.undoManagers.length; i++) {
this.undoManagers[i].onDocDelete(doc);
}
};

Connection.prototype.onDocOp = function(doc, op, undoOp, source, undoable, fixUp) {
for (var i = 0; i < this.undoManagers.length; i++) {
this.undoManagers[i].onDocOp(doc, op, undoOp, source, undoable, fixUp);
}
};
Loading