Closed davolokh closed 4 years ago
Hey @davolokh thank you so much for writing this up!
I'm definitely interested in figuring out the best way to make Slate collaboration-ready. I've been doing a bunch of research on OT/CRDT design to get the best solution. And if there's a way that the Slate core can remain unopinionated about OT vs. CRDT that would be awesome.
From all of my research it seems like OT is slightly more complex to implement, but is much more real-world-proven since it is what Google Docs, Dropbox Paper, etc. use to implement their collaborative editors. So I'm focusing on its needs first, which seem to overlap with CRDT a lot anyways.
It seems like what we need for the two is this:
Transform
needs to be composed of smaller, simpler "operations".Character
-level unique keys.(If anyone has additional requirements for either OT or CRDT that I need to add to that list and be aware of let me know!!)
Once we have those things, it seems like OT/CRDT can be implemented alongside Slate. But those things also aren't dependent on the core library changing much.
As for migrating it all to a Redux system, I'm slightly against it because it seems like a lot of overhead that makes Slate more confusing to people who aren't Redux-fluent, and it seems like it goes against how most React components are built. (For Hyperterm it actually makes a lot of sense, since they are essentially trying to make an entire app pluggable.)
I think that the current state can be changed to allow for "operations", by just exposing a single extra argument to onChange
handlers. Taking us from the current:
function onChange(state) {
// apply the change locally by setting new `state` value
}
To:
function onChange(state, change) {
// apply the change locally by setting new `state` value
// send the `change.operations` to the server
}
Which is passed a transform
argument which describes the transforms and operations that took place to change the state. (I'd actually like to rename this object from Transform
to Change
to make it more clear, and to remove the confusion with OT's definition for "transforms".)
And then the plugin handlers like onKeyDown
or onPaste
would simply return a Change
object instead of an entirely new state, which is the equivalent of removing the .apply()
call. Taking us from:
function onKeyDown(e, data, state, editor) {
// return a new `state` object by transforming the current one
return state
.transform()
.insertText('some new string')
.deleteForward()
.apply()
}
To:
function onKeyDown(e, data, state, editor) {
// return a `change` object by running transforms on the current `state`
return state
.change()
.insertText('some new string')
.deleteForward()
}
Let me know how that sounds! I'm still on vacation for a few more days, but hoping to get moving on this again next week.
Cc @SamyPesse too in case he has good thoughts, or in case I'm forgetting something obvious.
Hey there, just throwing this in as I'm following this issue here and it seems relevant as your deciding between CRTD/OT: http://arxiv.org/abs/1608.03960
I'm just reading/digesting the paper and eagerly waiting for the reference implementation to be released. We'd probably try to use it with the Store
interface you are coming up with.
Is there any progress about implementation of OT/CRDT on Slate? I'd just like to know about.
Would be great to expose the 'change(s)' in onChange and/or get any way to get a list of changes in order to sync with other clients.
Currently looking at syncing entire state between clients, with logic to throttle and ignore changes by the current clientID, which seems hacky
Real time collaboration would certainly be a great win for SlateJS. I'm sure you are aware of how ProseMirror is implementing it, which looks like an approach that should also be working with Slate in general:
http://prosemirror.net/guide/collab.html http://prosemirror.net/guide/transform.html#rebasing https://github.com/ProseMirror/website/tree/master/src/demo/collab
But I also wanted to point you to another great project by @dmonad called Yjs:
It is a general purpose framework for real time communication, with sample implementations for QuillJS and other text inputs. It provides some abstracted data types, which might be a good basis for additional communication.
@ianstormtaylor I'm curious if there's a branch where you've got these changes running so far. I'd love to contribute; getting Slate to work collaboratively is something we're very interested in.
This is somewhat a note to my future self (I don't have the time to invest right now, hopefully soon), but also for visibility and a vote for keeping Slate unopinionated about OT vs CRDT.
I'd like to take a stab at a plugin at some point to translate Slate transforms into the OT operations listed here:
https://github.com/ottypes/json0#summary-of-operations
This would make Slate play nicely with ShareDB and hopefully other collaborative OT stacks.
It would probably require a lot of the work for change hooks/events/whatever discussed in this issue to be knocked out first to get at the proper transform operations, but hopefully not too painful after that point.
I'm just getting my head around OT/CRDT, so I really don't actually understand the implications yet or how much it would be possible to stay decoupled, but +1 for just exposing the raw transform/change data and letting plugins handle the rest.
Is there any progress made on this? just curious to see if anything was started...
Nothing on my end! Still seeming pretty far out from having a window of time for it.
Wanting to do something is just as good as doing it.. right??! :bowtie:
Will something like this help: https://github.com/google/ot-crdt-papers (author post https://medium.com/@raphlinus/towards-a-unified-theory-of-operational-transformation-and-crdt-70485876f72f)
I will try to understand both this theory and how Slate work under the hood first 😄
How about using crjdt?
hi @ianstormtaylor I studied mostly all of the editors out there and everyone has some issues. I was working in draft-js until I reached to the stage of collaborative requirements. tried doing some hacks into draft-js, by calculating diff-patch and reach to a stage but as soon as undo/redo stack part came, everything failed. Came to your repository, wish I found it earlier and I liked the way you have structured the project and easy to understand coding. This time I am trying the collab part first and the changes you mentioned in your comment above seems a good start to try the hands on. Are you working on these? Do you have any timeline for this. Any way I can contribute to this to make things faster.
Edit: also one more question? Is there a way to do transform without inserting into undo stack currently? or that's why you are suggesting change/transform separately. : Found it
Hey all, thanks for the interest in this issue! To answer some of your questions...
I definitely still want this to happen. Slate core is very close, I think the last remaining change will be to make "transforms" the first-class thing that plugins return from event handlers, instead of right now how they return full-fledged State
instances. (Basically you'll just omit the .transform()
and .apply()
from the plugin code, and they'll chain together on a single transform instead.)
I'm going to focus on OT as our collaboration algorithm, instead of CRDT, because in general it's much more proven for the kind of work we want to do, and there's a decent amount of existing work that can help—ShareDB is probably the best way to go for getting it working. That said, if you really want/need CRDT feel free to charge ahead in that direction, and I'm down to keep Slate un-opinionated regardless. But I think for almost all of the cases people have brought up so far that OT is the better fit.
The collaboration aspect will be a lot of work. Feel free to throw progress, thoughts, etc. in this issue and we can all be updated if people are working on it.
I like the direction this is headed in! I've started some preliminary exploration of a slate-ottype library in the vein of https://github.com/ottypes/docs, and wanted to float some ideas:
addMark, insertNode, insertText, joinNode, moveNode, removeMark, removeNode, removeText, setMark, setNode, setSelection, splitNodeAtOffset, splitNode
), that's 78 different functions (13 choose 2)! Is that right? Maybe there are some shortcuts or symmetry to take advantage of.@thomsbg good insights! I totally agree. I think both rich-text
and json0
don't quite support our needs, but they are good for context of what's possible. There's also a json1
that Joseph has been working on again recently (on his personal GitHub).
I'd be very curious about your (or other's) take on the sparse traversal approach, and how that might apply to our nested tree.
I think in terms of operations we can do some smart things, but not totally sure, for example for remove_node
we can probably handle all cases of operations with a path
by just decrementing the overlapping index. Instead of needing to handle it for each combination. Same for insert but in reverse. Of course, it'll still be pretty complex.
Anyways, great write up! Would love to hear more thoughts if you dig deeper.
Hey @ianstormtaylor - I am watching this issue/thread with eager anticipation. Are you working on this stuff on a different branch? Wondering how long it would be before enough hooks are in place to have Slate working against a ShareDB in a sample repo?
Is this a long ways away? Or something we can plan on employing relatively soon?
Thanks!
Is there a consensus to use OT instead of CRDT?
If Slate used CRDT, it would allow it to sync without the need for a central authority like shareDB at all, and could just work out of the box with clients connected to each other without a server-side component. It would also make offline work and syncing much easier than with OT.
Y-js has been suggested earlier in this thread and my experience with it has been surprisingly good.
If there's still some room to influence the direction this feature is taking, I would vote for using CRDT or making it agnostic and easy to plug something like Y-js.
Hey @philcockfield, I wouldn't assume a short-term OT unless people contribute more pieces that are required for OT. Even fixes bugs that would prevent OT from working (like the inline
ones) would be helpful.
@olivoil I haven't seen much in terms of CRDT's actually being used in real-world scenarios, so there's a lot less prior art out there, if it's even possible. For that reason I'm personally going to opt towards OT implementation over time, it's just more proven. Using something like ShareDB would ensure that lots of the work is handled for us out of the box, which is a lot of upside.
Thank @ianstormtaylor.
The pros of CRDT that @olivoil outlines seem very nice - but I'm guessing that the various hooks necessary to get OT operating would also work for CRDT (I say naively, not in any way actually knowing!) @olivoil, is it reasonable that the necessary hooks would be applicable to both OT and CRDT?
Leaning on more extensive prior-art with OT (with ShareDB as a example lib) makes sense to me. Thanks @ianstormtaylor - and thanks for this wonderful library.
Thank you all for working on this extremely useful feature! I would really like something that could plug into the state manager (Redux in my case) and do the merges/transforms.
In the meantime, has anyone had any success with a poor man's version - such as (explicit) paragraph locking or other approaches that prevents concurrent updates?
@tvedtorama I too am super interested in any poor man's hacks
on the way to full collaborative editing.
I've been playing with the idea of a locking blocks based on who's editing, and putting a lot of "social presence" feedback in there, namely tiny avatars on each block, so it's clear why something is locked.
I have not yet implemented it though.
Also, I echo your sentiment, thank you everyone who's been working hard on this SUPER SUPER USEFUL feature.
@ianstormtaylor I have made a collaborative product, I'm the backend designer, we use ot algorithm, so I know what is needed by an ot collaborative editor.
Our current collaborative editor is a compose of ckeditor hand handsometable, customized a lot, and the code looks awkward.
I'm searching for a proper editor program to build our new collaborator editor on top of it. I looked at draft js, it's react model and immutable data type seems attractive. In theory, it's easy to diff immutable data, and get the changes, but I dig into draft and find it hard to separate the changed part from the whole state.
Then I found slate, see the documents, and I think slate is just one step away from collaborative editing, at least for ot algorithm.
I looked your answer to @davolokh, your idea is totally right, the last step is map a Transform to the change of the state. We should make use of immutable model, make proper abstract of a change. And then the upper level code can map this change to their operation model easily.
Different collaborative document model have different view of change. Google wave treat the whole document as a string, and properties like bold is anchored to the string by position. Some collaborative document use json model, and the property and texts are nodes of a tree.
So a good abstract of Change object is important for all possible collaborative model to build their own operation object.
Here's my experience of Draft.js / Slate / Quill. Worked on all 3 of them.
Draft.js - Very nice, if you are working as a solo user, completely lacking Collaboration part and if that's your need, forget it. And it will take years because it has to be completely rewritten from core. The community is very large, you get most of the answers to your questions. Although if you need to change something in core, you will have a lot of trouble, especially selection module.
Slate - Nice, but again collaboration is missing. although seems like @ianstormtaylor is interested in implementing this, but very slow progress is going on. And not sure if Slate will achieve this before Draft.js or not. Plus I really didn't understand why everything is Operation based task, if collaboration was not the intent from the very beginning. And sometimes editor becomes very heavy, especially if you copying pasting large content blocks from somewhere. Draft.js is very fast. I hope if Slate implements Collaboration before Draft.js it will be a big hit. Community is not very big for Slate, you are your own or if you're lucky maybe Ian will answer some of your questions.
Quill - Very Very Nice but sucks at many places. I've implemented all three and finally settled down on Quill, because collaboration is there and it's almost working fine. Sometimes a few places somethings goes wrong, mostly in undo/redo. But overall it's working ok. But and the big BUT is that it's not in React so I had to create a wrapper for it in React and I can't do interesting things beyond a certain level. Plus their Block system is too confusing, because it's entirely on DOM and every minor thing that you have to do you are mostly dependent on the DOM attributes. Community is again one of the problem, it's not big.
Final Thoughts: For now Quill is working fine for me. But I would anyday switch to Slate or Draft.js if they implement Collaboration.
@akashkamboj Now I haven't used Quill, so I'm not going to comment on that. But I've used both Draft and Slate quite extensively, and I have to disagree on some of your points. Especially this part:
Although seems like @ianstormtaylor is interested in implementing this, but very slow progress is going on. And not sure if Slate will achieve this before Draft.js or not. Plus I really didn't understand why everything is Operation based task, if collaboration was not the intent from the very beginning.
The reason everything is based on operations (or transformations) is that @ianstormtaylor indeed did intend to support collaborative editing from the very beginning. This is key, and one of the major reasons why I moved away from Draft.
I also don't feel that the progress on collaborative editing is slow. @ianstormtaylor is constantly updating the branch on which he works on it, and people are helping him test it. To me it seems that Slate soon will have all the pieces needed in place.
I also wonder a bit about this:
Community is not very big for Slate, you are your own or if you're lucky maybe Ian will answer some of your questions.
Things might have changed since I moved from Draft to Slate, but I actually prefer the community of Slate. The Slack channel is pretty active, and people are both friendly and helpful. While this is also true for Draft, the thing I like with Slate is that @ianstormtaylor is very open to thoughts from the community.
Which brings me to the other major reason for my move to Slate — Draft, compared to Slate, is incredibly reluctant to changes in core. Not because their maintainers are bad people (they're super nice and smart), but because Draft is used in a lot of different Facebook products. So each change has to go through each of those teams before being approved. None of FBs products require collaborative editing, and until any of them do, I highly doubt that support for it will be added. And even then, as you point out, that would require big changes to core.
Sorry if this came off as confrontational @akashkamboj. You're of course entitled to your opinions and experiences. I just wanted to share my view on the same topic, thanks for sharing yours. Also, I really don't mean to shit on Draft. I enjoyed working on that too, and they have some really nice people maintaining it. But for us, Slate has been better in almost every way, so big props to @ianstormtaylor!
The Slack channel is pretty active, and people are both friendly and helpful.
@tobiasandersen where's the Slack channel?
@philcockfield https://slate-slack.herokuapp.com/ :)
Hey all, the OT branch was merged in 0.22.0
so everything that unlocks OT should now be present in Slate's core! More specifically:
Change
objects are available for each change, with the exact operations that occurred.apply
and invert
operation helpers are exposed.I don't think Slate's core will actually add the OT logic itself, since it isn't used for all use cases and would add bloat. So instead, it would live in another package. If anyone wants to start collaborating on creating that package that would be amazing!
If you're working on that, and you run into any places that Slate's core is still missing functionality required, let me know and we can get it fixed. But I'm pretty sure it's all there.
@ianstormtaylor Awesome work, huge gain for Slate!
@ianstormtaylor - man!! Thanks so much for all this inspired work. This is so massively helpful to have OT.
If I understand this correctly, the Slate data model now have the granularity and structure to allow conflic-free edits and merges - which is excellent. For the moment, however, it's the application developer's job to do the actual merges when concurrent edits are received from peers (server signaling / webrtc). Is that correct?
Can we use existing tools for that, such as the y.js
library? Or would this model require it's own implementation?
Again, thank you for this great and inspiring work!
I've been playing around with Automerge lately (independently of Slate). It's a JSON CRDT that I think would be a perfect fit for Slate.
@aslakhellesoy, that looks really interesting! Let us know if you get it working with Slate, seems like a very cool option.
Whoa! I've been looking for a pure javascript JSON CRDT implementation for the past couple weeks.
I'm working on an offline-first app, and that could be huge for apps like mine that need to merge after offline collaboration.
Once I get to the actually-implementing-collaboration bit, I'll be taking more of a look.
@ianstormtaylor congrats on exposing the change operations! So cool to see the progress.
@ianstormtaylor I'm not currently working on Slate/Automerge integration, nor do I plan to in the near future. Maybe in the distant future.
OTOH I've just implemented CodeMirror/Automerge integration which could be a decent starting point for a Slate/Automerge plugin.
Hi! I try to use latest builds and info about operations is awesome. Thank you, @ianstormtaylor, it looks like that Slate is the editor of dreams 😄
If anyone's interested in working on an ottypes implementation for the slate.js model, hit me up! I'm working on an app that this would be perfect for :)
(And I've built some complex OT types before...)
@nornagon awesome. I love to take part in the project!
@lxcid cool! I'm hanging out on the Slack space if you want to coordinate :)
I’m in there as @lxcid as well! Let’s coordinate there! What’s your nick? I should be back on my desk in 5-6 hour time. :)
Sent from my iPhone
On 27 Sep 2017, at 1:50 PM, Jeremy Apthorp notifications@github.com wrote:
@lxcid cool! I'm hanging out on the Slack space if you want to coordinate :)
— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub, or mute the thread.
Are there any updates to documentation anywhere that calls out these new OT related features in some kind of collaborative editor context? Thanks.
Hello @ianstormtaylor thanks for the great work on this ...
We're currently considering switching from DraftJS to Slate .. for the specific reason of needing collaborative editing. I'm currently going through the API to see if it allows us to do what we need to do.
The question I have is, I'm now pulling out the Change
object from the onChange
function and sending the change.operations
to other users on the page .. on the receiving end, how do I apply these operations on the current state and generate a new state accordingly?
UPDATE The approach I have below is missing key pieces as I figured out after testing .. please see the comments below
Following up on my last comment ... I was able to figure out a few pieces of the puzzle, and I would like to share what I'm doing with everyone here.
I have 2 goals I'm trying to achieve:
To achieve the first goal, I'm doing the following:
onChange
functionChange
objectstate.change().applyOperations(operations)
onChange
function and Voilà the changes are appliedTo achieve the second goal, I intend to do the following:
componentDidUpdate
function, I pull out the new selection
from the updated state.selection
to raw format using the toJSON
methodfocusKey
and anchorKey
with relative paths instead of explicit keys using the state.document.getPath(key)
function. This step is important since keys are not unique identifiers and could be different from one slate instance to anotheranchorKey
and focusKey
back using the state.document.assertPath(path)
function to get the correct keys.Mark
that I call UserCursor
that decorates the text currently selected by the other user with a color background and adds a small tooltip above that text that shows the user's name or avatar. I add that mark to the selection range that was just received, using the function state.change().addMarkAtRange(Selection, Mark)
functionstate.change().removeMarkByKey()
function.As of now, I was able to finish 80% of the first goal, and it seems to be working well so far. However, I thought it would be a good idea to share my approach with anyone interested. I'm also interested in any feedback I can get.
@ianstormtaylor I'm also interested to know why some very useful functions like assertPath
, getPath
, and applyOperations
are not mentioned in the docs. Are there other ways to get the same outcome? is there a plan to deprecate any of these functions?
Thanks @mshibl - that is very helpful for me to look at your process.
You'll have trouble with that approach if two users edit a document concurrently. The documents will get out of sync. You need something like OT if you want to handle concurrent edits successfully. On Sun, Oct 1, 2017 at 17:05 Phil Cockfield notifications@github.com wrote:
Thanks @mshibl https://github.com/mshibl - that is very helpful for me to look at your process.
— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/ianstormtaylor/slate/issues/259#issuecomment-333416493, or mute the thread https://github.com/notifications/unsubscribe-auth/AAKjAMR6CgIP6d01xYz43PrBqG9Zr5zZks5soCjHgaJpZM4JohSO .
@nornagon indeed after testing this turned out to be true .. now I see the point of having OT..
@philcockfield please be careful of the approach I've mentioned above as it does produce inconsistency between the two pages
@mshibl I tried almost similar approach about a week ago. As @nornagon already mention, it doesn't work right. I want to try https://github.com/automerge/automerge now. I will post here if I get any success.
@devilcoders @mshibl I've also looked into this and currently also looking into automerge. On that point, I'm running into a slight problem using Slate's Immutable data structure and automerge's built-in use of JSON. It should be solvable since they're all objects at the end of the day... What do you think about setting up a channel on the Slate slack to discuss ideas on collaboration in general (whether it be OT/CRDT)?
Automerge doesn't appear to handle merging edits within a string, so that will only work so long as your users don't want to edit the same paragraph at the same time :)
@nornagon if a string is represented as an array of characters (strings of length 1), Automerge will merge them nicely. That's what I do in automerge-codemirror
https://operational-transformation.github.io/ot-for-javascript.html seems like what we need .. I'll try to build something similar that works with Slate
@yatskevich and me did a research and tried to design an appropriate architecture for supporting collaborative editing. There are some key points with explanation below.
The main idea is to implement a kind of Redux-like behavior within Slate.
<Editor store={Store} … />
The goal of having Store is ability to have clear separation and understandable flow.UseCase:
This way allows us to have ability to manage operations in any suitable way (CRDT, OT, etc.) within Store.
Let’s see how to handle text paste operation in more details:
pasteText
dispatched to Store (data: {position, text}
) No need to get current state, call transform() and return new state. Just dispatch an Action.CollaborativeStore:
pasteText
operation into a set of atomicinsertCharacter
operationsSimpleStore:
In our mind such architecture allows Slate to be more flexible and extensible. But this is not a completely analyzed proposal - just an idea.
And a few pictures for clarification.
Workflow Chart:![fullsizerender](https://cloud.githubusercontent.com/assets/2715874/17812946/f88bff2e-6631-11e6-8055-1723fe784848.jpg)
Implementation example![fullsizerender 5](https://cloud.githubusercontent.com/assets/2715874/17812955/01e4e914-6632-11e6-8b70-b2b299e9a017.jpg)
pastText
in depth:P.S. Possibly, Store should send Transfroms (not Actions) to Server and get Transforms back.
@ianstormtaylor: Could you please take a look at this and share your opinion?