Compare commits

...

5 Commits

Author SHA1 Message Date
Kevin Jahns
7744993dde Deploy 12.1.0 2016-10-29 21:45:33 +02:00
Kevin Jahns
cf3969dff6 fix richtext example 2016-10-13 17:28:22 +02:00
Kevin Jahns
b8dd7d1862 Deploy 12.0.3 2016-10-07 21:00:42 +02:00
Kevin Jahns
9e9f238b12 Deploy 12.0.2 2016-10-05 00:59:56 +02:00
Kevin Jahns
723fc77627 Deploy 12.0.1 2016-09-27 16:17:27 +02:00
7 changed files with 265 additions and 142 deletions

View File

@@ -8,7 +8,7 @@ Y({
}, },
connector: { connector: {
name: 'websockets-client', name: 'websockets-client',
room: 'richtext-example-quill-1.0' room: 'richtext-example-quill-1.0-test'
}, },
sourceDir: '/bower_components', sourceDir: '/bower_components',
share: { share: {
@@ -37,4 +37,3 @@ Y({
// bind quill to richtext type // bind quill to richtext type
y.share.richtext.bind(window.quill) y.share.richtext.bind(window.quill)
}) })

View File

@@ -1,8 +1,8 @@
# ![Yjs](http://y-js.org/images/yjs.png) # ![Yjs](http://y-js.org/images/yjs.png)
Yjs is a framework for p2p shared editing on structured data like (rich-)text, json, and XML. Yjs is a framework for offline-first p2p shared editing on structured data like text, richtext, json, or XML.
It is similar to [ShareJs] and [OpenCoweb], but easy to use. It is fairly easy to get started, as Yjs hides most of the complexity of concurrent editing.
For additional information, demos, and tutorials visit [y-js.org](http://y-js.org/). For additional information, demos, and tutorials visit [y-js.org](http://y-js.org/).
### Extensions ### Extensions
@@ -71,6 +71,19 @@ require('y-text')(Y)
// do the same for all modules you want to use // do the same for all modules you want to use
``` ```
### ES6 Syntax
```
import Y from 'yjs'
import yArray from 'y-array'
import yWebsocketsClient from 'y-webrtc'
import yMemory from 'y-memory'
import yArray from 'y-array'
import yMap from 'y-map'
import yText from 'y-text'
// ..
Y.extend(yArray, yWebsocketsClient, yMemory, yArray, yMap, yText /*, .. */)
```
# Text editing example # Text editing example
Install dependencies Install dependencies
``` ```
@@ -78,17 +91,17 @@ bower i yjs y-memory y-webrtc y-array y-text
``` ```
Here is a simple example of a shared textarea Here is a simple example of a shared textarea
``` ```HTML
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<body> <body>
<script src="./bower_components/yjs/y.js"></script> <script src="./bower_components/yjs/y.js"></script>
<!-- Yjs automatically includes all missing dependencies (browser only) -->
<script> <script>
Y({ Y({
db: { db: {
name: 'memory' // use memory database adapter. name: 'memory' // use memory database adapter.
// name: 'indexeddb' // name: 'indexeddb' // use indexeddb database adapter instead for offline apps
// name: 'leveldb'
}, },
connector: { connector: {
name: 'webrtc', // use webrtc connector name: 'webrtc', // use webrtc connector
@@ -104,9 +117,9 @@ Here is a simple example of a shared textarea
// The Yjs instance `y` is available // The Yjs instance `y` is available
// y.share.* contains the shared types // y.share.* contains the shared types
// Bind the textarea to y.share.textarea // Bind `y.share.textarea` to `<textarea/>`
y.share.textarea.bind(document.querySelector('textarea')) y.share.textarea.bind(document.querySelector('textarea'))
} })
</script> </script>
<textarea></textarea> <textarea></textarea>
</body> </body>
@@ -121,6 +134,9 @@ Report _any_ issues to the [Github issue page](https://github.com/y-js/yjs/issue
# API # API
### Y(options) ### Y(options)
* Y.extend(module1, module2, ..)
* Add extensions to Y
* `Y.extend(require('y-webrtc'))` has the same semantics as `require('y-webrtc')(Y)`
* options.db * options.db
* Will be forwarded to the database adapter. Specify the database adaper on `options.db.name`. * Will be forwarded to the database adapter. Specify the database adaper on `options.db.name`.
* Have a look at the used database adapter repository to see all available options. * Have a look at the used database adapter repository to see all available options.
@@ -242,10 +258,7 @@ This is a complete rewrite of the 0.5 version of Yjs. Since Yjs 0.6.0 it is poss
I created this framework during my bachelor thesis at the chair of computer science 5 [(i5)](http://dbis.rwth-aachen.de/cms), RWTH University. Since December 2014 I'm working on Yjs as a part of my student worker job at the i5. I created this framework during my bachelor thesis at the chair of computer science 5 [(i5)](http://dbis.rwth-aachen.de/cms), RWTH University. Since December 2014 I'm working on Yjs as a part of my student worker job at the i5.
## License ## License
Yjs is licensed under the [MIT License](./LICENSE.txt). Yjs is licensed under the [MIT License](./LICENSE).
<yjs@dbis.rwth-aachen.de> <yjs@dbis.rwth-aachen.de>
[ShareJs]: https://github.com/share/ShareJS
[OpenCoweb]: https://github.com/opencoweb/coweb/wiki

View File

@@ -1,6 +1,6 @@
{ {
"name": "yjs", "name": "yjs",
"version": "12.0.0", "version": "12.1.0",
"homepage": "y-js.org", "homepage": "y-js.org",
"authors": [ "authors": [
"Kevin Jahns <kevin.jahns@rwth-aachen.de>" "Kevin Jahns <kevin.jahns@rwth-aachen.de>"

157
y.es6
View File

@@ -2,6 +2,9 @@
/* @flow */ /* @flow */
'use strict' 'use strict'
function canRead (auth) { return auth === 'read' || auth === 'write' }
function canWrite (auth) { return auth === 'write' }
module.exports = function (Y/* :any */) { module.exports = function (Y/* :any */) {
class AbstractConnector { class AbstractConnector {
/* :: /* ::
@@ -55,6 +58,8 @@ module.exports = function (Y/* :any */) {
this.syncStep2 = Promise.resolve() this.syncStep2 = Promise.resolve()
this.broadcastOpBuffer = [] this.broadcastOpBuffer = []
this.protocolVersion = 11 this.protocolVersion = 11
this.authInfo = opts.auth || null
this.checkAuth = opts.checkAuth || function () { return Promise.resolve('write') } // default is everyone has write access
} }
reconnect () { reconnect () {
} }
@@ -67,6 +72,16 @@ module.exports = function (Y/* :any */) {
this.whenSyncedListeners = [] this.whenSyncedListeners = []
return this.y.db.stopGarbageCollector() return this.y.db.stopGarbageCollector()
} }
repair () {
console.info('Repairing the state of Yjs. This can happen if messages get lost, and Yjs detects that something is wrong. If this happens often, please report an issue here: https://github.com/y-js/yjs/issues')
for (var name in this.connections) {
this.connections[name].isSynced = false
}
this.isSynced = false
this.currentSyncTarget = null
this.broadcastedHB = false
this.findNextSyncTarget()
}
setUserId (userId) { setUserId (userId) {
if (this.userId == null) { if (this.userId == null) {
this.userId = userId this.userId = userId
@@ -78,6 +93,9 @@ module.exports = function (Y/* :any */) {
onUserEvent (f) { onUserEvent (f) {
this.userEventListeners.push(f) this.userEventListeners.push(f)
} }
removeUserEventListener (f) {
this.userEventListeners = this.userEventListeners.filter(g => { f !== g })
}
userLeft (user) { userLeft (user) {
if (this.connections[user] != null) { if (this.connections[user] != null) {
delete this.connections[user] delete this.connections[user]
@@ -154,7 +172,8 @@ module.exports = function (Y/* :any */) {
type: 'sync step 1', type: 'sync step 1',
stateSet: stateSet, stateSet: stateSet,
deleteSet: deleteSet, deleteSet: deleteSet,
protocolVersion: conn.protocolVersion protocolVersion: conn.protocolVersion,
auth: conn.authInfo
}) })
}) })
} else { } else {
@@ -208,7 +227,7 @@ module.exports = function (Y/* :any */) {
*/ */
receiveMessage (sender/* :UserId */, message/* :Message */) { receiveMessage (sender/* :UserId */, message/* :Message */) {
if (sender === this.userId) { if (sender === this.userId) {
return return Promise.resolve()
} }
if (this.debug) { if (this.debug) {
console.log(`receive ${sender} -> ${this.userId}: ${message.type}`, JSON.parse(JSON.stringify(message))) // eslint-disable-line console.log(`receive ${sender} -> ${this.userId}: ${message.type}`, JSON.parse(JSON.stringify(message))) // eslint-disable-line
@@ -223,14 +242,36 @@ module.exports = function (Y/* :any */) {
type: 'sync stop', type: 'sync stop',
protocolVersion: this.protocolVersion protocolVersion: this.protocolVersion
}) })
return return Promise.reject('Incompatible protocol version')
} }
if (message.type === 'sync step 1') { if (message.auth != null && this.connections[sender] != null) {
// authenticate using auth in message
var auth = this.checkAuth(message.auth, this.y)
this.connections[sender].auth = auth
auth.then(auth => {
for (var f of this.userEventListeners) {
f({
action: 'userAuthenticated',
user: sender,
auth: auth
})
}
})
} else if (this.connections[sender] != null && this.connections[sender].auth == null) {
// authenticate without otherwise
this.connections[sender].auth = this.checkAuth(null, this.y)
}
if (this.connections[sender] != null && this.connections[sender].auth != null) {
return this.connections[sender].auth.then((auth) => {
if (message.type === 'sync step 1' && canRead(auth)) {
let conn = this let conn = this
let m = message let m = message
this.y.db.requestTransaction(function *() { this.y.db.requestTransaction(function *() {
var currentStateSet = yield* this.getStateSet() var currentStateSet = yield* this.getStateSet()
if (canWrite(auth)) {
yield* this.applyDeleteSet(m.deleteSet) yield* this.applyDeleteSet(m.deleteSet)
}
var ds = yield* this.getDeleteSet() var ds = yield* this.getDeleteSet()
var ops = yield* this.getOperations(m.stateSet) var ops = yield* this.getOperations(m.stateSet)
@@ -239,7 +280,8 @@ module.exports = function (Y/* :any */) {
os: ops, os: ops,
stateSet: currentStateSet, stateSet: currentStateSet,
deleteSet: ds, deleteSet: ds,
protocolVersion: this.protocolVersion protocolVersion: this.protocolVersion,
auth: this.authInfo
}) })
if (this.forwardToSyncingClients) { if (this.forwardToSyncingClients) {
conn.syncingClients.push(sender) conn.syncingClients.push(sender)
@@ -258,7 +300,7 @@ module.exports = function (Y/* :any */) {
} }
conn._setSyncedWith(sender) conn._setSyncedWith(sender)
}) })
} else if (message.type === 'sync step 2') { } else if (message.type === 'sync step 2' && canWrite(auth)) {
let conn = this let conn = this
var broadcastHB = !this.broadcastedHB var broadcastHB = !this.broadcastedHB
this.broadcastedHB = true this.broadcastedHB = true
@@ -293,7 +335,7 @@ module.exports = function (Y/* :any */) {
this.syncStep2.then(function () { this.syncStep2.then(function () {
self._setSyncedWith(sender) self._setSyncedWith(sender)
}) })
} else if (message.type === 'update') { } else if (message.type === 'update' && canWrite(auth)) {
if (this.forwardToSyncingClients) { if (this.forwardToSyncingClients) {
for (var client of this.syncingClients) { for (var client of this.syncingClients) {
this.send(client, message) this.send(client, message)
@@ -309,6 +351,10 @@ module.exports = function (Y/* :any */) {
} }
this.y.db.apply(message.ops) this.y.db.apply(message.ops)
} }
})
} else {
return Promise.reject('Unable to deliver message')
}
} }
_setSyncedWith (user) { _setSyncedWith (user) {
var conn = this.connections[user] var conn = this.connections[user]
@@ -441,11 +487,21 @@ module.exports = function (Y) {
} }
}, },
whenTransactionsFinished: function () { whenTransactionsFinished: function () {
var self = this
return new Promise(function (resolve, reject) {
// The connector first has to send the messages to the db.
// Wait for the checkAuth-function to resolve
// The test lib only has a simple checkAuth function: `() => Promise.resolve()`
// Just add a function to the event-queue, in order to wait for the event.
// TODO: this may be buggy in test applications (but it isn't be for real-life apps)
setTimeout(function () {
var ps = [] var ps = []
for (var name in this.users) { for (var name in self.users) {
ps.push(this.users[name].y.db.whenTransactionsFinished()) ps.push(self.users[name].y.db.whenTransactionsFinished())
} }
return Promise.all(ps) Promise.all(ps).then(resolve, reject)
}, 0)
})
}, },
flushOne: function flushOne () { flushOne: function flushOne () {
var bufs = [] var bufs = []
@@ -471,8 +527,9 @@ module.exports = function (Y) {
delete buff[sender] delete buff[sender]
} }
var user = globalRoom.users[userId] var user = globalRoom.users[userId]
user.receiveMessage(m[0], m[1]) return user.receiveMessage(m[0], m[1]).then(function () {
return user.y.db.whenTransactionsFinished() return user.y.db.whenTransactionsFinished()
}, function () {})
} else { } else {
return false return false
} }
@@ -489,8 +546,7 @@ module.exports = function (Y) {
} }
globalRoom.whenTransactionsFinished().then(nextFlush) globalRoom.whenTransactionsFinished().then(nextFlush)
} else { } else {
setTimeout(function () { c = globalRoom.flushOne()
var c = globalRoom.flushOne()
if (c) { if (c) {
c.then(function () { c.then(function () {
globalRoom.whenTransactionsFinished().then(nextFlush) globalRoom.whenTransactionsFinished().then(nextFlush)
@@ -498,7 +554,6 @@ module.exports = function (Y) {
} else { } else {
resolve() resolve()
} }
}, 0)
} }
} }
globalRoom.whenTransactionsFinished().then(nextFlush) globalRoom.whenTransactionsFinished().then(nextFlush)
@@ -524,7 +579,7 @@ module.exports = function (Y) {
this.syncingClientDuration = 0 this.syncingClientDuration = 0
} }
receiveMessage (sender, m) { receiveMessage (sender, m) {
super.receiveMessage(sender, JSON.parse(JSON.stringify(m))) return super.receiveMessage(sender, JSON.parse(JSON.stringify(m)))
} }
send (userId, message) { send (userId, message) {
var buffer = globalRoom.buffers[userId] var buffer = globalRoom.buffers[userId]
@@ -571,7 +626,7 @@ module.exports = function (Y) {
if (buff[sender].length === 0) { if (buff[sender].length === 0) {
delete buff[sender] delete buff[sender]
} }
this.receiveMessage(m[0], m[1]) yield this.receiveMessage(m[0], m[1])
} }
yield self.whenTransactionsFinished() yield self.whenTransactionsFinished()
}) })
@@ -696,6 +751,41 @@ module.exports = function (Y /* :any */) {
if (this.gcTimeout > 0) { if (this.gcTimeout > 0) {
garbageCollect() garbageCollect()
} }
this.repairCheckInterval = !opts.repairCheckInterval ? 6000 : opts.repairCheckInterval
this.opsReceivedTimestamp = new Date()
this.startRepairCheck()
}
startRepairCheck () {
var os = this
if (this.repairCheckInterval > 0) {
this.repairCheckIntervalHandler = setInterval(function repairOnMissingOperations () {
/*
Case 1. No ops have been received in a while (new Date() - os.opsReceivedTimestamp > os.repairCheckInterval)
- 1.1 os.listenersById is empty. Then the state was correct the whole time. -> Nothing to do (nor to update)
- 1.2 os.listenersById is not empty.
* Then the state was incorrect for at least {os.repairCheckInterval} seconds.
* -> Remove everything in os.listenersById and sync again (connector.repair())
Case 2. An op has been received in the last {os.repairCheckInterval } seconds.
It is not yet necessary to check for faulty behavior. Everything can still resolve itself. Wait for more messages.
If nothing was received for a while and os.listenersById is still not emty, we are in case 1.2
-> Do nothing
Baseline here is: we really only have to catch case 1.2..
*/
if (
new Date() - os.opsReceivedTimestamp > os.repairCheckInterval &&
Object.keys(os.listenersById).length > 0 // os.listenersById is not empty
) {
// haven't received operations for over {os.repairCheckInterval} seconds, resend state vector
os.listenersById = {}
os.opsReceivedTimestamp = new Date() // update so you don't send repair several times in a row
os.y.connector.repair()
}
}, this.repairCheckInterval)
}
}
stopRepairCheck () {
clearInterval(this.repairCheckIntervalHandler)
} }
queueGarbageCollector (id) { queueGarbageCollector (id) {
if (this.y.isConnected()) { if (this.y.isConnected()) {
@@ -791,6 +881,7 @@ module.exports = function (Y /* :any */) {
* destroy () { * destroy () {
clearInterval(this.gcInterval) clearInterval(this.gcInterval)
this.gcInterval = null this.gcInterval = null
this.stopRepairCheck()
for (var key in this.initializedTypes) { for (var key in this.initializedTypes) {
var type = this.initializedTypes[key] var type = this.initializedTypes[key]
if (type._destroy != null) { if (type._destroy != null) {
@@ -830,12 +921,14 @@ module.exports = function (Y /* :any */) {
/* /*
Apply a list of operations. Apply a list of operations.
* we save a timestamp, because we received new operations that could resolve ops in this.listenersById (see this.startRepairCheck)
* get a transaction * get a transaction
* check whether all Struct.*.requiredOps are in the OS * check whether all Struct.*.requiredOps are in the OS
* check if it is an expected op (otherwise wait for it) * check if it is an expected op (otherwise wait for it)
* check if was deleted, apply a delete operation after op was applied * check if was deleted, apply a delete operation after op was applied
*/ */
apply (ops) { apply (ops) {
this.opsReceivedTimestamp = new Date()
for (var i = 0; i < ops.length; i++) { for (var i = 0; i < ops.length; i++) {
var o = ops[i] var o = ops[i]
if (o.id == null || o.id[0] !== this.y.connector.userId) { if (o.id == null || o.id[0] !== this.y.connector.userId) {
@@ -2225,7 +2318,7 @@ module.exports = function (Y/* :any */) {
} }
if (this.store.forwardAppliedOperations) { if (this.store.forwardAppliedOperations) {
var ops = [] var ops = []
ops.push({struct: 'Delete', target: [d[0], d[1]], length: del[2]}) ops.push({struct: 'Delete', target: [del[0], del[1]], length: del[2]})
this.store.y.connector.broadcastOps(ops) this.store.y.connector.broadcastOps(ops)
} }
} }
@@ -2648,7 +2741,7 @@ module.exports = function (Y /* : any*/) {
try { try {
this.eventListeners[i](event) this.eventListeners[i](event)
} catch (e) { } catch (e) {
console.error('User events must not throw Errors!') console.error('Your observer threw an error. This error was caught so that Yjs still can ensure data consistency! In order to debug this error you have to check "Pause On Caught Exceptions"', e)
} }
} }
} }
@@ -3368,6 +3461,7 @@ module.exports = Y
Y.requiringModules = requiringModules Y.requiringModules = requiringModules
Y.extend = function (name, value) { Y.extend = function (name, value) {
if (arguments.length === 2 && typeof name === 'string') {
if (value instanceof Y.utils.CustomTypeDefinition) { if (value instanceof Y.utils.CustomTypeDefinition) {
Y[name] = value.parseArguments Y[name] = value.parseArguments
} else { } else {
@@ -3377,10 +3471,21 @@ Y.extend = function (name, value) {
requiringModules[name].resolve() requiringModules[name].resolve()
delete requiringModules[name] delete requiringModules[name]
} }
} else {
for (var i = 0; i < arguments.length; i++) {
var f = arguments[i]
if (typeof f === 'function') {
f(Y)
} else {
throw new Error('Expected function!')
}
}
}
} }
Y.requestModules = requestModules Y.requestModules = requestModules
function requestModules (modules) { function requestModules (modules) {
var sourceDir = Y.sourceDir || '/bower_components'
// determine if this module was compiled for es5 or es6 (y.js vs. y.es6) // determine if this module was compiled for es5 or es6 (y.js vs. y.es6)
// if Insert.execute is a Function, then it isnt a generator.. // if Insert.execute is a Function, then it isnt a generator..
// then load the es5(.js) files.. // then load the es5(.js) files..
@@ -3394,7 +3499,7 @@ function requestModules (modules) {
// module does not exist // module does not exist
if (typeof window !== 'undefined' && window.Y !== 'undefined') { if (typeof window !== 'undefined' && window.Y !== 'undefined') {
var imported = document.createElement('script') var imported = document.createElement('script')
imported.src = Y.sourceDir + '/' + modulename + '/' + modulename + extention imported.src = sourceDir + '/' + modulename + '/' + modulename + extention
document.head.appendChild(imported) document.head.appendChild(imported)
let requireModule = {} let requireModule = {}
@@ -3445,15 +3550,15 @@ type YOptions = {
*/ */
function Y (opts/* :YOptions */) /* :Promise<YConfig> */ { function Y (opts/* :YOptions */) /* :Promise<YConfig> */ {
if (opts.sourceDir != null) {
Y.sourceDir = opts.sourceDir
}
opts.types = opts.types != null ? opts.types : [] opts.types = opts.types != null ? opts.types : []
var modules = [opts.db.name, opts.connector.name].concat(opts.types) var modules = [opts.db.name, opts.connector.name].concat(opts.types)
for (var name in opts.share) { for (var name in opts.share) {
modules.push(opts.share[name]) modules.push(opts.share[name])
} }
Y.sourceDir = opts.sourceDir
return new Promise(function (resolve, reject) { return new Promise(function (resolve, reject) {
setTimeout(function () {
Y.requestModules(modules).then(function () {
if (opts == null) reject('An options object is expected! ') if (opts == null) reject('An options object is expected! ')
else if (opts.connector == null) reject('You must specify a connector! (missing connector property)') else if (opts.connector == null) reject('You must specify a connector! (missing connector property)')
else if (opts.connector.name == null) reject('You must specify connector name! (missing connector.name property)') else if (opts.connector.name == null) reject('You must specify connector name! (missing connector.name property)')
@@ -3461,15 +3566,21 @@ function Y (opts/* :YOptions */) /* :Promise<YConfig> */ {
else if (opts.connector.name == null) reject('You must specify db name! (missing db.name property)') else if (opts.connector.name == null) reject('You must specify db name! (missing db.name property)')
else if (opts.share == null) reject('You must specify a set of shared types!') else if (opts.share == null) reject('You must specify a set of shared types!')
else { else {
opts = Y.utils.copyObject(opts)
opts.connector = Y.utils.copyObject(opts.connector)
opts.db = Y.utils.copyObject(opts.db)
opts.share = Y.utils.copyObject(opts.share)
setTimeout(function () {
Y.requestModules(modules).then(function () {
var yconfig = new YConfig(opts) var yconfig = new YConfig(opts)
yconfig.db.whenUserIdSet(function () { yconfig.db.whenUserIdSet(function () {
yconfig.init(function () { yconfig.init(function () {
resolve(yconfig) resolve(yconfig)
}) })
}) })
}
}).catch(reject) }).catch(reject)
}, 0) }, 0)
}
}) })
} }

File diff suppressed because one or more lines are too long

6
y.js

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long