-
Notifications
You must be signed in to change notification settings - Fork 453
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
Add Presence functionality #322
Conversation
0b34105
to
7098830
Compare
Woohoo! Thrilled to see this fresh take on presence. |
Let's got a step further here, and completely remove the notion of presence from We can also completely decouple presence from Docs altogether, by having a notion of subscribing to presence channels (eg mouse cursor position on a screen might be a notion of presence that is not coupled to a Doc). The Doc presence should be an extension of this more generic presence. So the API might look something like: const presence = connection.getDocPresence(collection, id);
// Start listening for presence
presence.subscribe((err) => {...});
// Handle incoming presence (eg update a cursor position)
presence.on('receive', (remotePresence) => {...});
// Create a instances of local presence
const localPresence1 = presence.create(id1);
const localPresence2 = presence.create(id2);
// Presences can send independently
localPresence1.submit({...});
localPresence1.destroy((err) => {...});
// Can unsubscribe, but still broadcast
presence.unsubscribe((err) => {...});
localPresence2.submit({...});
// Broadcast a removal of all local presence, and free up local memory
presence.destroy((err) => {...}); |
f8eaf7e
to
bd4c874
Compare
I think the one thing I still find a little bit weird about its current incarnation is that if you're not subscribed, you won't receive any requests for your presence, so you won't inform any new subscribers. For example:
Maybe this is an edge case we don't need to think about too hard? The mainline case will presumably be all clients being subscribed all the time. I guess the other thing to think about is potentially garbage collecting stale clients who disconnected abruptly without a chance to update other clients. Maybe we can handle this in the |
bd4c874
to
8863d40
Compare
Greetings @alecgibson I'm looking at this work with fresh eyes and would like to try it out. Before diving into trying to craft a working example myself I thought I'd ask, do you have any working examples that use this implementation for presence features? Thanks! |
Also @nateps @ericyhwang I'm curious if there is a possibility that this implementation might get merged at some point. I know you've got a lot to juggle but just curious on a high level, is the possibility there? I'm again evaluating the |
@curran I haven't got an example working at the moment, because the API was a bit unstable through a couple of PR meetings. However, if you have a look at the tests, the usage should hopefully be pretty self-evident. Note that the If you want to try to get something up and running, I'd love the feedback. A very quick overview of the API as it currently stands (NB: using const doc = connection.get('books', 'northern-lights');
await doc.subscribe();
// This presence object represents all presence on the Doc. Can have multiple remote and
// local presences, available on `remotePresences` and `localPresences` properties
const presence = connection.getDocPresence('books', 'northern-lights');
presence.on('receive', (id, remotePresence) => {/** handle presence **/});
await presence.subscribe();
const localPresence = presence.create('user-1');
localPresence.submit({...}); The main piece of the puzzle that's missing is a localPresence.submit({index: 4, length: 0}); // uses the same structure as Quill
presence.on('receive', (id, remotePresence) => {
// Handle presence.
// remotePresence will have the same structure as above: {index: number; length: number}
}); Since the |
@@ -571,15 +576,15 @@ Doc.prototype._otApply = function(op, source) { | |||
|
|||
// The 'before op' event enables clients to pull any necessary data out of | |||
// the snapshot before it gets changed | |||
this.emit('before op', op.op, source); | |||
this.emit('before op', op.op, source, op.src); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's think about if we can get rid of this extra src
emission. We basically need to know if the op is our own op in RemoteDocPresence._handleOp
, and in Backend.transformPresenceToLatestVersion
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's keep this, but bury it one level deep in an object with a src
property for forwards-compatibility and self-documentation. We should also update the README
.
c72c0fb
to
9e8b166
Compare
@ericyhwang this should be ready for review. I've updated the I think the only outstanding issue is figuring out what to do with emitting |
Woohoo! Looking forward to giving the examples a spin. Will let you know how it goes. |
Local presence IDs should be unique. If consumers do not want the responsibility of taking care of this (and don't care about what ID is assigned to them), then we will automatically assign a random ID for them.
fd85b73
to
929d515
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM! Let's finally get this merged in 🚀
Nate says he's OK with adding you as an owner to the NPM sharedb
package, so I've gone and done so.
It's nearing the end of the workday for me, so I'm not going to merge+publish now. If you'd like to do the merge and publish in the morning your time, go for it!
Otherwise, we can do so during the meeting tomorrow and write release notes. I'd like to be sure and credit Greg and Curran for their work on presence, too.
Great work guys! I'm really happy to see this feature complete. 🎉 |
Amazing! Really really excited about this. Thank you everyone for your hard work here. |
Hey guys, this work is brilliant. A quick question, I can see a PR for json0 and an issue open for json1 to support a transformPresence method. Would that be something that will be merged/added soon? Also, in general, should json0 or json1 be used? Thanks, Neil |
Hi @neilcarv ! This is something near and dear to my heart and I'm glad you asked. Current status is: there's no demo code for the new implementation of ShareDB presence used with JSON0. I plan to work on one soon, but haven't been able to dig into it yet. Here's a PR agains JSON0 that implements the OLD version of presence https://github.com/ottypes/json0/pull/31/files . I would love to see this updated to the presently published API. I'm not sure how far it will diverge, but hopefully getting it to work should be relatively painless. I'd also like to see this demo ported to use the new presence implementation https://github.com/datavis-tech/json0-presence-demo I hope to take a stab at this some time but if anyone else has interest, feel free to have a crack at it! Re: JSON0 vs. JSON1 - folks generally are still using JSON0. I don't know of anyone actively using JSON1. It looks really nice, but there's more of an ecosystem around JSON0. /cc @houshuang |
This change adds support for presence transformations, which are being [added to ShareDB][1]. In order to support presence updates, this change adds support for the optional `transformPresence` method, which simply reuses the existing `transformCursor` method, but also: - applies changes to the `length` of a range - keeps existing metadata - returns `null` if no range has been provided [1]: share/sharedb#322
This change adds support for presence transformations, which are being [added to ShareDB][1]. In order to support presence updates, this change adds support for the optional `transformPresence` method, which simply reuses the existing `transformCursor` method, but also: - applies changes to the `length` of a range - keeps existing metadata - returns `null` if no range has been provided [1]: share/sharedb#322
This change adds support for the `transformPresence()` method that [`sharedb` uses][1]. We add support for both `text0` and `json0`. `text0` ------- The `text0` implementation leans on the existing [`transformPosition`][2], and takes its form and tests from [`rich-text`][3]. Its shape takes the form: ```js { index: 3, length: 5, } ``` Where: - `index` is the cursor position - `length` is the selection length (`0` for a collapsed selection) `json0` ------- The `json0` implementation has limited functionality because of the limitations of the `json0` type itself: we handle list moves `lm`, but cannot infer any information when moving objects around the tree, because the `oi` and `od` operations are destructive. However, it will attempt to transform embedded subtypes that support presence. Its shape takes the form: ```js { p: ['key', 123], v: {}, } ``` Where: - `p` is the path to the client's position within the document - `v` is the presence value The presence value `v` can take any arbitrary value (in simple cases it may even be omitted entirely). The exception to this is when using subtypes, where `v` should take the presence shape defined by the subtype. For example, when using `text0`: ```js { p: ['key'], v: {index: 5, length: 0}, } ``` [1]: share/sharedb#322 [2]: https://github.com/ottypes/json0/blob/90a3ae26364c4fa3b19b6df34dad46707a704421/lib/text0.js#L147 [3]: ottypes/rich-text#32
This change adds support for the `transformPresence()` method that [`sharedb` uses][1]. We add support for both `text0` and `json0`. `text0` ------- The `text0` implementation leans on the existing [`transformPosition`][2], and takes its form and tests from [`rich-text`][3]. Its shape takes the form: ```js { index: 3, length: 5, } ``` Where: - `index` is the cursor position - `length` is the selection length (`0` for a collapsed selection) `json0` ------- The `json0` implementation has limited functionality because of the limitations of the `json0` type itself: we handle list moves `lm`, but cannot infer any information when moving objects around the tree, because the `oi` and `od` operations are destructive. However, it will attempt to transform embedded subtypes that support presence. Its shape takes the form: ```js { p: ['key', 123], v: {}, } ``` Where: - `p` is the path to the client's position within the document - `v` is the presence value The presence value `v` can take any arbitrary value (in simple cases it may even be omitted entirely). The exception to this is when using subtypes, where `v` should take the presence shape defined by the subtype. For example, when using `text0`: ```js { p: ['key'], v: {index: 5, length: 0}, } ``` [1]: share/sharedb#322 [2]: https://github.com/ottypes/json0/blob/90a3ae26364c4fa3b19b6df34dad46707a704421/lib/text0.js#L147 [3]: ottypes/rich-text#32
This change adds support for the `transformPresence()` method that [`sharedb` uses][1]. We add support for both `text0` and `json0`. `text0` ------- The `text0` implementation leans on the existing [`transformPosition`][2], and takes its form and tests from [`rich-text`][3]. Its shape takes the form: ```js { index: 3, length: 5, } ``` Where: - `index` is the cursor position - `length` is the selection length (`0` for a collapsed selection) `json0` ------- The `json0` implementation has limited functionality because of the limitations of the `json0` type itself: we handle list moves `lm`, but cannot infer any information when moving objects around the tree, because the `oi` and `od` operations are destructive. However, it will attempt to transform embedded subtypes that support presence. Its shape takes the form: ```js { p: ['key', 123], v: {}, } ``` Where: - `p` is the path to the client's position within the document - `v` is the presence value The presence value `v` can take any arbitrary value (in simple cases it may even be omitted entirely). The exception to this is when using subtypes, where `v` should take the presence shape defined by the subtype. For example, when using `text0`: ```js { p: ['key'], v: {index: 5, length: 0}, } ``` [1]: share/sharedb#322 [2]: https://github.com/ottypes/json0/blob/90a3ae26364c4fa3b19b6df34dad46707a704421/lib/text0.js#L147 [3]: ottypes/rich-text#32
This change adds support for the `transformPresence()` method that [`sharedb` uses][1]. We add support for both `text0` and `json0`. `text0` ------- The `text0` implementation leans on the existing [`transformPosition`][2], and takes its form and tests from [`rich-text`][3]. Its shape takes the form: ```js { index: 3, length: 5, } ``` Where: - `index` is the cursor position - `length` is the selection length (`0` for a collapsed selection) `json0` ------- The `json0` implementation has limited functionality because of the limitations of the `json0` type itself: we handle list moves `lm`, but cannot infer any information when moving objects around the tree, because the `oi` and `od` operations are destructive. However, it will attempt to transform embedded subtypes that support presence. Its shape takes the form: ```js { p: ['key', 123], v: {}, } ``` Where: - `p` is the path to the client's position within the document - `v` is the presence value The presence value `v` can take any arbitrary value (in simple cases it may even be omitted entirely). The exception to this is when using subtypes, where `v` should take the presence shape defined by the subtype. For example, when using `text0`: ```js { p: ['key'], v: {index: 5, length: 0}, } ``` [1]: share/sharedb#322 [2]: https://github.com/ottypes/json0/blob/90a3ae26364c4fa3b19b6df34dad46707a704421/lib/text0.js#L147 [3]: ottypes/rich-text#32
This change adds support for the `transformPresence()` method that [`sharedb` uses][1]. We add support for both `text0` and `json0`. `text0` ------- The `text0` implementation leans on the existing [`transformPosition`][2], and takes its form and tests from [`rich-text`][3]. Its shape takes the form: ```js { index: 3, length: 5, } ``` Where: - `index` is the cursor position - `length` is the selection length (`0` for a collapsed selection) `json0` ------- The `json0` implementation has limited functionality because of the limitations of the `json0` type itself: we handle list moves `lm`, but cannot infer any information when moving objects around the tree, because the `oi` and `od` operations are destructive. However, it will attempt to transform embedded subtypes that support presence. Its shape takes the form: ```js { p: ['key', 123], v: {}, } ``` Where: - `p` is the path to the client's position within the document - `v` is the presence value The presence value `v` can take any arbitrary value (in simple cases it may even be omitted entirely). The exception to this is when using subtypes, where `v` should take the presence shape defined by the subtype. For example, when using `text0`: ```js { p: ['key'], v: {index: 5, length: 0}, } ``` [1]: share/sharedb#322 [2]: https://github.com/ottypes/json0/blob/90a3ae26364c4fa3b19b6df34dad46707a704421/lib/text0.js#L147 [3]: ottypes/rich-text#32
This change adds support for the `transformPresence()` method that [`sharedb` uses][1]. We add support for both `text0` and `json0`. `text0` ------- The `text0` implementation leans on the existing [`transformPosition`][2], and takes its form and tests from [`rich-text`][3]. Its shape takes the form: ```js { index: 3, length: 5, } ``` Where: - `index` is the cursor position - `length` is the selection length (`0` for a collapsed selection) `json0` ------- The `json0` implementation has limited functionality because of the limitations of the `json0` type itself: we handle list moves `lm`, but cannot infer any information when moving objects around the tree, because the `oi` and `od` operations are destructive. However, it will attempt to transform embedded subtypes that support presence. Its shape takes the form: ```js { p: ['key', 123], v: {}, } ``` Where: - `p` is the path to the client's position within the document - `v` is the presence value The presence value `v` can take any arbitrary value (in simple cases it may even be omitted entirely). The exception to this is when using subtypes, where `v` should take the presence shape defined by the subtype. For example, when using `text0`: ```js { p: ['key'], v: {index: 5, length: 0}, } ``` [1]: share/sharedb#322 [2]: https://github.com/ottypes/json0/blob/90a3ae26364c4fa3b19b6df34dad46707a704421/lib/text0.js#L147 [3]: ottypes/rich-text#32
This change adds support for the `transformPresence()` method that [`sharedb` uses][1]. We add support for both `text0` and `json0`. `text0` ------- The `text0` implementation leans on the existing [`transformPosition`][2], and takes its form and tests from [`rich-text`][3]. Its shape takes the form: ```js { index: 3, length: 5, } ``` Where: - `index` is the cursor position - `length` is the selection length (`0` for a collapsed selection) `json0` ------- The `json0` implementation has limited functionality because of the limitations of the `json0` type itself: we handle list moves `lm`, but cannot infer any information when moving objects around the tree, because the `oi` and `od` operations are destructive. However, it will attempt to transform embedded subtypes that support presence. Its shape takes the form: ```js { p: ['key', 123], v: {}, } ``` Where: - `p` is the path to the client's position within the document - `v` is the presence value The presence value `v` can take any arbitrary value (in simple cases it may even be omitted entirely). The exception to this is when using subtypes, where `v` should take the presence shape defined by the subtype. For example, when using `text0`: ```js { p: ['key'], v: {index: 5, length: 0}, } ``` [1]: share/sharedb#322 [2]: https://github.com/ottypes/json0/blob/90a3ae26364c4fa3b19b6df34dad46707a704421/lib/text0.js#L147 [3]: ottypes/rich-text#32
This change adds support for the `transformPresence()` method that [`sharedb` uses][1]. We add support for both `text0` and `json0`. `text0` ------- The `text0` implementation leans on the existing [`transformPosition`][2], and takes its form and tests from [`rich-text`][3]. Its shape takes the form: ```js { index: 3, length: 5, } ``` Where: - `index` is the cursor position - `length` is the selection length (`0` for a collapsed selection) `json0` ------- The `json0` implementation has limited functionality because of the limitations of the `json0` type itself: we handle list moves `lm`, but cannot infer any information when moving objects around the tree, because the `oi` and `od` operations are destructive. However, it will attempt to transform embedded subtypes that support presence. Its shape takes the form: ```js { p: ['key', 123], v: {}, } ``` Where: - `p` is the path to the client's position within the document - `v` is the presence value The presence value `v` can take any arbitrary value (in simple cases it may even be omitted entirely). The exception to this is when using subtypes, where `v` should take the presence shape defined by the subtype. For example, when using `text0`: ```js { p: ['key'], v: {index: 5, length: 0}, } ``` [1]: share/sharedb#322 [2]: https://github.com/ottypes/json0/blob/90a3ae26364c4fa3b19b6df34dad46707a704421/lib/text0.js#L147 [3]: ottypes/rich-text#32
This change adds the ability for clients to broadcast information about
"Presence" - the notion of a client's position or state in a particular
document. This might be represent a cursor in a text document, or a
highlighted field in a more complex JSON document, or any other
transient, current information about a client that shouldn't necessarily
be stored in the document's chain of ops.
The main complication that this feature solves is the issue of keeping
presence correctly associated with the version of a
Doc
it was createdat. For example, in a "naive" implementation of presence, presence
information can arrive ahead of or behind ops, which - in a text-based
example - can cause the cursor to "jitter" around the change. Using the
ShareDB implementation will ensure that the presence is correctly
transformed against any ops, and will ensure that presence information
is always consistent with the version of the document. We also locally
transform existing presence, which should help to keep (static) remote
presence correctly positioned, independent of latency.
In order to facilitate this, the feature must be used with an OT type
that supports presence. The only requirement for supporting presence is
the support of a
transformPresence
method:presence
Object: the presence data being transformed. The typewill define this shape to be whatever is appropriate for the type.
op
Op: the operation against which to transform the presenceisOwnOperation
: boolean: whether the presence and the op have thesame "owner". This information can be useful for some types to break
ties when transforming a presence, for example as used in
rich-text
This work is based on the work by @gkubisa and @curran, but with
the following aims:
Doc
class as much as possible, andinstead use lifecycle hooks
Doc
subscriptions(although in practice, the two are obviously tightly coupled)
Doc
on the sameConnection