-
Notifications
You must be signed in to change notification settings - Fork 452
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
Sync ephemeral presence data #207
base: master
Are you sure you want to change the base?
Changes from 6 commits
8705c4f
af84be6
09edf92
9121baf
4dbefd1
2ef8181
6b687db
1489e36
a4499a5
33c7264
8ff4b33
0ff380d
d67dd6a
e8ec215
9c291b2
c15448f
173bf3a
054d34d
15cdd1d
cfca37f
5e009d1
642ded6
56b726b
762496a
e4c5e6d
428c46a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,9 @@ | ||
language: node_js | ||
node_js: | ||
- 6 | ||
- 5 | ||
- 4 | ||
- 0.10 | ||
- "9" | ||
- "8" | ||
- "6" | ||
- "4" | ||
script: "npm run jshint && npm run test-cover" | ||
# Send coverage data to Coveralls | ||
after_script: "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js" |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,6 +19,7 @@ tracker](https://github.com/share/sharedb/issues). | |
|
||
- Realtime synchronization of any JSON document | ||
- Concurrent multi-user collaboration | ||
- Realtime synchronization of any ephemeral "presence" data | ||
- 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) | ||
|
@@ -57,6 +58,10 @@ initial data. Then you can submit editing operations on the document (using | |
OT). Finally you can delete the document with a delete operation. By | ||
default, ShareDB stores all operations forever - nothing is truly deleted. | ||
|
||
## User presence synchronization | ||
|
||
Presence data represents a user and is automatically synchronized between all clients subscribed to the same document. Its format is defined by the document's [OT Type](https://github.com/ottypes/docs), for example it may contain a user ID and a cursor position in a text document. All clients can modify their own presence data and receive a read-only version of other client's data. Presence data is automatically cleared when a client unsubscribes from the document or disconnects. It is also automatically transformed against applied operations, so that it still makes sense in the context of a modified document, for example a cursor position may be automatically advanced when a user types at the beginning of a text document. | ||
|
||
## Server API | ||
|
||
### Initialization | ||
|
@@ -221,6 +226,9 @@ Unique document ID | |
`doc.data` _(Object)_ | ||
Document contents. Available after document is fetched or subscribed to. | ||
|
||
`doc.presence` _(Object)_ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is where the following comment from @nateps and @ericyhwang apply:
More concretely, the docs here could change from The name |
||
Each property under `doc.presence` contains presence data shared by a client subscribed to this document. The property name is an empty string for this client's data and connection IDs for other clients' data. | ||
|
||
`doc.fetch(function(err) {...})` | ||
Populate the fields on `doc` with a snapshot of the document from the server. | ||
|
||
|
@@ -250,6 +258,9 @@ An operation was applied to the data. `source` will be `false` for ops received | |
`doc.on('del', function(data, source) {...})` | ||
The document was deleted. Document contents before deletion are passed in as an argument. `source` will be `false` for ops received from the server and defaults to `true` for ops generated locally. | ||
|
||
`doc.on('presence', function(srcList) {...})` | ||
Presence data has changed. `srcList` is an Array of `doc.presence` property names for which values have changed. | ||
|
||
`doc.on('error', function(err) {...})` | ||
There was an error fetching the document or applying an operation. | ||
|
||
|
@@ -283,6 +294,11 @@ Invokes the given callback function after | |
|
||
Note that `whenNothingPending` does NOT wait for pending `model.query()` calls. | ||
|
||
`doc.submitPresence(presenceData[, function(err) {...}])` | ||
Set local presence data and publish it for other clients. | ||
`presenceData` structure depends on the document type. | ||
Presence is synchronized only when subscribed to the document. | ||
|
||
### Class: `ShareDB.Query` | ||
|
||
`query.ready` _(Boolean)_ | ||
|
@@ -358,6 +374,9 @@ 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 presence | ||
* 4025 - Not subscribed to document | ||
* 4026 - Presence data superseded | ||
|
||
### 5000 - Internal error | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
var hat = require('hat'); | ||
var util = require('./util'); | ||
var types = require('./types'); | ||
var ShareDBError = require('./error'); | ||
|
||
/** | ||
* Agent deserializes the wire protocol messages received from the stream and | ||
|
@@ -25,6 +26,9 @@ function Agent(backend, stream) { | |
// Map from queryId -> emitter | ||
this.subscribedQueries = {}; | ||
|
||
// The max presence sequence number received from the client. | ||
this.maxPresenceSeq = 0; | ||
|
||
// We need to track this manually to make sure we don't reply to messages | ||
// after the stream was closed. | ||
this.closed = false; | ||
|
@@ -98,10 +102,17 @@ Agent.prototype._subscribeToStream = function(collection, id, stream) { | |
console.error('Doc subscription stream error', collection, id, data.error); | ||
return; | ||
} | ||
if (data.a === 'p') { | ||
// Send other clients' presence data | ||
if (data.src !== agent.clientId) agent.send(data); | ||
return; | ||
} | ||
if (agent._isOwnOp(collection, data)) return; | ||
agent._sendOp(collection, id, data); | ||
}); | ||
stream.on('end', function() { | ||
var presence = agent._createPresence(collection, id); | ||
agent.backend.sendPresence(presence); | ||
// The op stream is done sending, so release its reference | ||
var streams = agent.subscribedDocs[collection]; | ||
if (!streams) return; | ||
|
@@ -268,6 +279,13 @@ Agent.prototype._checkRequest = function(request) { | |
// Bulk request | ||
if (request.c != null && typeof request.c !== 'string') return 'Invalid collection'; | ||
if (typeof request.b !== 'object') return 'Invalid bulk subscribe data'; | ||
} else if (request.a === 'p') { | ||
// Presence | ||
if (typeof request.c !== 'string') return 'Invalid collection'; | ||
if (typeof request.d !== 'string') return 'Invalid id'; | ||
if (typeof request.v !== 'number' || request.v < 0) return 'Invalid version'; | ||
if (typeof request.seq !== 'number' || request.seq <= 0) return 'Invalid seq'; | ||
if (typeof request.r !== 'undefined' && typeof request.r !== 'boolean') return 'Invalid "request reply" value'; | ||
} | ||
}; | ||
|
||
|
@@ -300,6 +318,9 @@ Agent.prototype._handleMessage = function(request, callback) { | |
var op = this._createOp(request); | ||
if (!op) return callback({code: 4000, message: 'Invalid op message'}); | ||
return this._submit(request.c, request.d, op, callback); | ||
case 'p': | ||
var presence = this._createPresence(request.c, request.d, request.p, request.v, request.r, request.seq); | ||
return this._presence(presence, callback); | ||
default: | ||
callback({code: 4000, message: 'Invalid or unknown message'}); | ||
} | ||
|
@@ -582,3 +603,30 @@ Agent.prototype._createOp = function(request) { | |
return new DeleteOp(src, request.seq, request.v, request.del); | ||
} | ||
}; | ||
|
||
Agent.prototype._presence = function(presence, callback) { | ||
if (presence.seq <= this.maxPresenceSeq) { | ||
return callback(new ShareDBError(4026, 'Presence data superseded')); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. good practice to make the callbacks async no ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed, thanks 👍 |
||
} | ||
this.maxPresenceSeq = presence.seq; | ||
if (!this.subscribedDocs[presence.c] || !this.subscribedDocs[presence.c][presence.d]) { | ||
return callback(new ShareDBError(4025, 'Cannot send presence. Not subscribed to document: ' + presence.c + ' ' + presence.d)); | ||
} | ||
this.backend.sendPresence(presence, function(err) { | ||
if (err) return callback(err); | ||
callback(null, { seq: presence.seq }); | ||
}); | ||
}; | ||
|
||
Agent.prototype._createPresence = function(collection, id, data, version, requestReply, seq) { | ||
return { | ||
a: 'p', | ||
src: this.clientId, | ||
seq: seq != null ? seq : this.maxPresenceSeq, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. right sorry I never use equality operator |
||
c: collection, | ||
d: id, | ||
p: data, | ||
v: version, | ||
r: requestReply | ||
}; | ||
}; |
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.
This
.editorconfig
file seems extranious and IMO should be removed.