Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
957d650f81 | ||
|
|
9769968c1c | ||
|
|
1e30a877e6 | ||
|
|
7744993dde | ||
|
|
cf3969dff6 | ||
|
|
549ab76b42 |
@@ -36,17 +36,4 @@ Y({
|
|||||||
});
|
});
|
||||||
// bind quill to richtext type
|
// bind quill to richtext type
|
||||||
y.share.richtext.bind(window.quill)
|
y.share.richtext.bind(window.quill)
|
||||||
})
|
})
|
||||||
|
|
||||||
Y({
|
|
||||||
db: {
|
|
||||||
name: 'indexeddb'
|
|
||||||
},
|
|
||||||
connector: {
|
|
||||||
name: 'websockets-client',
|
|
||||||
room: 'test42'
|
|
||||||
},
|
|
||||||
share: {
|
|
||||||
state : 'Map'
|
|
||||||
}
|
|
||||||
}).then((y) => { window.y = y })
|
|
||||||
32
README.md
32
README.md
@@ -1,8 +1,8 @@
|
|||||||
|
|
||||||
# 
|
# 
|
||||||
|
|
||||||
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
|
||||||
@@ -42,7 +42,7 @@ Connectors, Databases, and Types are available as modules that extend Yjs. Here
|
|||||||
|[richtext](https://github.com/y-js/y-richtext) | Collaborate on rich text. Supports two way binding to the [Quill Rich Text Editor](http://quilljs.com/)|
|
|[richtext](https://github.com/y-js/y-richtext) | Collaborate on rich text. Supports two way binding to the [Quill Rich Text Editor](http://quilljs.com/)|
|
||||||
|
|
||||||
## Use it!
|
## Use it!
|
||||||
Install Yjs, and its modules with [bower](http://bower.io/), or [npm](https://www.npmjs.org/package/yjs).
|
Install Yjs, and its modules with [bower](http://bower.io/), or [npm](https://www.npmjs.org/package/yjs).
|
||||||
|
|
||||||
### Bower
|
### Bower
|
||||||
```
|
```
|
||||||
@@ -91,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
|
||||||
@@ -117,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>
|
||||||
@@ -134,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.
|
||||||
@@ -195,14 +198,20 @@ The promise returns an instance of Y. We denote it with a lower case `y`.
|
|||||||
* Force to disconnect this instance from the other instances
|
* Force to disconnect this instance from the other instances
|
||||||
* y.connector.reconnect()
|
* y.connector.reconnect()
|
||||||
* Try to reconnect to the other instances (needs to be supported by the connector)
|
* Try to reconnect to the other instances (needs to be supported by the connector)
|
||||||
* Not supported by y-xmpp
|
* Not supported by y-xmpp
|
||||||
* y.destroy()
|
* y.close()
|
||||||
* Destroy this object.
|
* Destroy this object.
|
||||||
* Destroys all types (they will throw weird errors if you still use them)
|
* Destroys all types (they will throw weird errors if you still use them)
|
||||||
* Disconnects from the other instances (via connector)
|
* Disconnects from the other instances (via connector)
|
||||||
|
* Returns a promise
|
||||||
|
* y.destroy()
|
||||||
|
* calls y.close()
|
||||||
* Removes all data from the database
|
* Removes all data from the database
|
||||||
|
* Returns a promise
|
||||||
* y.db.stopGarbageCollector()
|
* y.db.stopGarbageCollector()
|
||||||
* Stop the garbage collector. Call y.db.garbageCollect() to continue garbage collection
|
* Stop the garbage collector. Call y.db.garbageCollect() to continue garbage collection
|
||||||
|
* y.db.gc :: Boolean
|
||||||
|
* Whether gc is turned on
|
||||||
* y.db.gcTimeout :: Number (defaults to 50000 ms)
|
* y.db.gcTimeout :: Number (defaults to 50000 ms)
|
||||||
* Time interval between two garbage collect cycles
|
* Time interval between two garbage collect cycles
|
||||||
* It is required that all instances exchanged all messages after two garbage collect cycles (after 100000 ms per default)
|
* It is required that all instances exchanged all messages after two garbage collect cycles (after 100000 ms per default)
|
||||||
@@ -259,6 +268,3 @@ 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
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "yjs",
|
"name": "yjs",
|
||||||
"version": "12.0.3",
|
"version": "12.1.2",
|
||||||
"homepage": "y-js.org",
|
"homepage": "y-js.org",
|
||||||
"authors": [
|
"authors": [
|
||||||
"Kevin Jahns <kevin.jahns@rwth-aachen.de>"
|
"Kevin Jahns <kevin.jahns@rwth-aachen.de>"
|
||||||
|
|||||||
413
y.es6
413
y.es6
@@ -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 () {
|
||||||
}
|
}
|
||||||
@@ -88,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]
|
||||||
@@ -137,13 +145,8 @@ module.exports = function (Y/* :any */) {
|
|||||||
this.whenSyncedListeners.push(f)
|
this.whenSyncedListeners.push(f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/*
|
|
||||||
|
|
||||||
returns false, if there is no sync target
|
|
||||||
true otherwise
|
|
||||||
*/
|
|
||||||
findNextSyncTarget () {
|
findNextSyncTarget () {
|
||||||
if (this.currentSyncTarget != null || this.isSynced) {
|
if (this.currentSyncTarget != null) {
|
||||||
return // "The current sync has not finished!"
|
return // "The current sync has not finished!"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,20 +167,25 @@ 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 {
|
||||||
this.y.db.requestTransaction(function *() {
|
if (!conn.isSynced) {
|
||||||
// it is crucial that isSynced is set at the time garbageCollectAfterSync is called
|
this.y.db.requestTransaction(function *() {
|
||||||
conn.isSynced = true
|
if (!conn.isSynced) {
|
||||||
yield* this.garbageCollectAfterSync()
|
// it is crucial that isSynced is set at the time garbageCollectAfterSync is called
|
||||||
// call whensynced listeners
|
conn.isSynced = true
|
||||||
for (var f of conn.whenSyncedListeners) {
|
yield* this.garbageCollectAfterSync()
|
||||||
f()
|
// call whensynced listeners
|
||||||
}
|
for (var f of conn.whenSyncedListeners) {
|
||||||
conn.whenSyncedListeners = []
|
f()
|
||||||
})
|
}
|
||||||
|
conn.whenSyncedListeners = []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
send (uid, message) {
|
send (uid, message) {
|
||||||
@@ -218,7 +226,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
|
||||||
@@ -233,91 +241,118 @@ 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) {
|
||||||
let conn = this
|
// authenticate using auth in message
|
||||||
let m = message
|
var auth = this.checkAuth(message.auth, this.y)
|
||||||
this.y.db.requestTransaction(function *() {
|
this.connections[sender].auth = auth
|
||||||
var currentStateSet = yield* this.getStateSet()
|
auth.then(auth => {
|
||||||
yield* this.applyDeleteSet(m.deleteSet)
|
for (var f of this.userEventListeners) {
|
||||||
|
f({
|
||||||
var ds = yield* this.getDeleteSet()
|
action: 'userAuthenticated',
|
||||||
var ops = yield* this.getOperations(m.stateSet)
|
user: sender,
|
||||||
conn.send(sender, {
|
auth: auth
|
||||||
type: 'sync step 2',
|
|
||||||
os: ops,
|
|
||||||
stateSet: currentStateSet,
|
|
||||||
deleteSet: ds,
|
|
||||||
protocolVersion: this.protocolVersion
|
|
||||||
})
|
|
||||||
if (this.forwardToSyncingClients) {
|
|
||||||
conn.syncingClients.push(sender)
|
|
||||||
setTimeout(function () {
|
|
||||||
conn.syncingClients = conn.syncingClients.filter(function (cli) {
|
|
||||||
return cli !== sender
|
|
||||||
})
|
|
||||||
conn.send(sender, {
|
|
||||||
type: 'sync done'
|
|
||||||
})
|
|
||||||
}, 5000) // TODO: conn.syncingClientDuration)
|
|
||||||
} else {
|
|
||||||
conn.send(sender, {
|
|
||||||
type: 'sync done'
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
conn._setSyncedWith(sender)
|
|
||||||
})
|
})
|
||||||
} else if (message.type === 'sync step 2') {
|
} else if (this.connections[sender] != null && this.connections[sender].auth == null) {
|
||||||
let conn = this
|
// authenticate without otherwise
|
||||||
var broadcastHB = !this.broadcastedHB
|
this.connections[sender].auth = this.checkAuth(null, this.y)
|
||||||
this.broadcastedHB = true
|
}
|
||||||
var db = this.y.db
|
if (this.connections[sender] != null && this.connections[sender].auth != null) {
|
||||||
var defer = {}
|
return this.connections[sender].auth.then((auth) => {
|
||||||
defer.promise = new Promise(function (resolve) {
|
if (message.type === 'sync step 1' && canRead(auth)) {
|
||||||
defer.resolve = resolve
|
let conn = this
|
||||||
})
|
let m = message
|
||||||
this.syncStep2 = defer.promise
|
|
||||||
let m /* :MessageSyncStep2 */ = message
|
this.y.db.requestTransaction(function *() {
|
||||||
db.requestTransaction(function * () {
|
var currentStateSet = yield* this.getStateSet()
|
||||||
yield* this.applyDeleteSet(m.deleteSet)
|
if (canWrite(auth)) {
|
||||||
this.store.apply(m.os)
|
yield* this.applyDeleteSet(m.deleteSet)
|
||||||
db.requestTransaction(function * () {
|
}
|
||||||
var ops = yield* this.getOperations(m.stateSet)
|
|
||||||
if (ops.length > 0) {
|
var ds = yield* this.getDeleteSet()
|
||||||
if (!broadcastHB) { // TODO: consider to broadcast here..
|
var ops = yield* this.getOperations(m.stateSet)
|
||||||
conn.send(sender, {
|
conn.send(sender, {
|
||||||
type: 'update',
|
type: 'sync step 2',
|
||||||
ops: ops
|
os: ops,
|
||||||
})
|
stateSet: currentStateSet,
|
||||||
|
deleteSet: ds,
|
||||||
|
protocolVersion: this.protocolVersion,
|
||||||
|
auth: this.authInfo
|
||||||
|
})
|
||||||
|
if (this.forwardToSyncingClients) {
|
||||||
|
conn.syncingClients.push(sender)
|
||||||
|
setTimeout(function () {
|
||||||
|
conn.syncingClients = conn.syncingClients.filter(function (cli) {
|
||||||
|
return cli !== sender
|
||||||
|
})
|
||||||
|
conn.send(sender, {
|
||||||
|
type: 'sync done'
|
||||||
|
})
|
||||||
|
}, 5000) // TODO: conn.syncingClientDuration)
|
||||||
} else {
|
} else {
|
||||||
// broadcast only once!
|
conn.send(sender, {
|
||||||
conn.broadcastOps(ops)
|
type: 'sync done'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
conn._setSyncedWith(sender)
|
||||||
|
})
|
||||||
|
} else if (message.type === 'sync step 2' && canWrite(auth)) {
|
||||||
|
let conn = this
|
||||||
|
var broadcastHB = !this.broadcastedHB
|
||||||
|
this.broadcastedHB = true
|
||||||
|
var db = this.y.db
|
||||||
|
var defer = {}
|
||||||
|
defer.promise = new Promise(function (resolve) {
|
||||||
|
defer.resolve = resolve
|
||||||
|
})
|
||||||
|
this.syncStep2 = defer.promise
|
||||||
|
let m /* :MessageSyncStep2 */ = message
|
||||||
|
db.requestTransaction(function * () {
|
||||||
|
yield* this.applyDeleteSet(m.deleteSet)
|
||||||
|
this.store.apply(m.os)
|
||||||
|
db.requestTransaction(function * () {
|
||||||
|
var ops = yield* this.getOperations(m.stateSet)
|
||||||
|
if (ops.length > 0) {
|
||||||
|
if (!broadcastHB) { // TODO: consider to broadcast here..
|
||||||
|
conn.send(sender, {
|
||||||
|
type: 'update',
|
||||||
|
ops: ops
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// broadcast only once!
|
||||||
|
conn.broadcastOps(ops)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defer.resolve()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} else if (message.type === 'sync done') {
|
||||||
|
var self = this
|
||||||
|
this.syncStep2.then(function () {
|
||||||
|
self._setSyncedWith(sender)
|
||||||
|
})
|
||||||
|
} else if (message.type === 'update' && canWrite(auth)) {
|
||||||
|
if (this.forwardToSyncingClients) {
|
||||||
|
for (var client of this.syncingClients) {
|
||||||
|
this.send(client, message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
defer.resolve()
|
if (this.y.db.forwardAppliedOperations) {
|
||||||
})
|
var delops = message.ops.filter(function (o) {
|
||||||
})
|
return o.struct === 'Delete'
|
||||||
} else if (message.type === 'sync done') {
|
})
|
||||||
var self = this
|
if (delops.length > 0) {
|
||||||
this.syncStep2.then(function () {
|
this.broadcastOps(delops)
|
||||||
self._setSyncedWith(sender)
|
}
|
||||||
})
|
}
|
||||||
} else if (message.type === 'update') {
|
this.y.db.apply(message.ops)
|
||||||
if (this.forwardToSyncingClients) {
|
|
||||||
for (var client of this.syncingClients) {
|
|
||||||
this.send(client, message)
|
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
if (this.y.db.forwardAppliedOperations) {
|
} else {
|
||||||
var delops = message.ops.filter(function (o) {
|
return Promise.reject('Unable to deliver message')
|
||||||
return o.struct === 'Delete'
|
|
||||||
})
|
|
||||||
if (delops.length > 0) {
|
|
||||||
this.broadcastOps(delops)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.y.db.apply(message.ops)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_setSyncedWith (user) {
|
_setSyncedWith (user) {
|
||||||
@@ -451,11 +486,21 @@ module.exports = function (Y) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
whenTransactionsFinished: function () {
|
whenTransactionsFinished: function () {
|
||||||
var ps = []
|
var self = this
|
||||||
for (var name in this.users) {
|
return new Promise(function (resolve, reject) {
|
||||||
ps.push(this.users[name].y.db.whenTransactionsFinished())
|
// The connector first has to send the messages to the db.
|
||||||
}
|
// Wait for the checkAuth-function to resolve
|
||||||
return Promise.all(ps)
|
// 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 = []
|
||||||
|
for (var name in self.users) {
|
||||||
|
ps.push(self.users[name].y.db.whenTransactionsFinished())
|
||||||
|
}
|
||||||
|
Promise.all(ps).then(resolve, reject)
|
||||||
|
}, 0)
|
||||||
|
})
|
||||||
},
|
},
|
||||||
flushOne: function flushOne () {
|
flushOne: function flushOne () {
|
||||||
var bufs = []
|
var bufs = []
|
||||||
@@ -481,8 +526,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
|
||||||
}
|
}
|
||||||
@@ -499,16 +545,14 @@ 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)
|
})
|
||||||
})
|
} else {
|
||||||
} else {
|
resolve()
|
||||||
resolve()
|
}
|
||||||
}
|
|
||||||
}, 0)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
globalRoom.whenTransactionsFinished().then(nextFlush)
|
globalRoom.whenTransactionsFinished().then(nextFlush)
|
||||||
@@ -534,7 +578,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]
|
||||||
@@ -581,7 +625,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()
|
||||||
})
|
})
|
||||||
@@ -669,12 +713,18 @@ module.exports = function (Y /* :any */) {
|
|||||||
}
|
}
|
||||||
this.gc1 = [] // first stage
|
this.gc1 = [] // first stage
|
||||||
this.gc2 = [] // second stage -> after that, remove the op
|
this.gc2 = [] // second stage -> after that, remove the op
|
||||||
this.gcTimeout = !opts.gcTimeout ? 50000 : opts.gcTimeouts
|
this.gc = opts.gc == null || opts.gc
|
||||||
|
if (this.gc) {
|
||||||
|
this.gcTimeout = !opts.gcTimeout ? 50000 : opts.gcTimeout
|
||||||
|
} else {
|
||||||
|
this.gcTimeout = -1
|
||||||
|
}
|
||||||
|
|
||||||
function garbageCollect () {
|
function garbageCollect () {
|
||||||
return os.whenTransactionsFinished().then(function () {
|
return os.whenTransactionsFinished().then(function () {
|
||||||
if (os.gc1.length > 0 || os.gc2.length > 0) {
|
if (os.gc1.length > 0 || os.gc2.length > 0) {
|
||||||
if (!os.y.isConnected()) {
|
if (!os.y.connector.isSynced) {
|
||||||
console.warn('gc should be empty when disconnected!')
|
console.warn('gc should be empty when not synced!')
|
||||||
}
|
}
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
os.requestTransaction(function * () {
|
os.requestTransaction(function * () {
|
||||||
@@ -743,7 +793,7 @@ module.exports = function (Y /* :any */) {
|
|||||||
clearInterval(this.repairCheckIntervalHandler)
|
clearInterval(this.repairCheckIntervalHandler)
|
||||||
}
|
}
|
||||||
queueGarbageCollector (id) {
|
queueGarbageCollector (id) {
|
||||||
if (this.y.isConnected()) {
|
if (this.y.connector.isSynced && this.gc) {
|
||||||
this.gc1.push(id)
|
this.gc1.push(id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -798,7 +848,7 @@ module.exports = function (Y /* :any */) {
|
|||||||
TODO: rename this function
|
TODO: rename this function
|
||||||
|
|
||||||
Rulez:
|
Rulez:
|
||||||
* Only gc if this user is online
|
* Only gc if this user is online & gc turned on
|
||||||
* The most left element in a list must not be gc'd.
|
* The most left element in a list must not be gc'd.
|
||||||
=> There is at least one element in the list
|
=> There is at least one element in the list
|
||||||
|
|
||||||
@@ -807,7 +857,9 @@ module.exports = function (Y /* :any */) {
|
|||||||
* addToGarbageCollector (op, left) {
|
* addToGarbageCollector (op, left) {
|
||||||
if (
|
if (
|
||||||
op.gc == null &&
|
op.gc == null &&
|
||||||
op.deleted === true
|
op.deleted === true &&
|
||||||
|
this.store.gc &&
|
||||||
|
this.store.y.connector.isSynced
|
||||||
) {
|
) {
|
||||||
var gc = false
|
var gc = false
|
||||||
if (left != null && left.deleted === true) {
|
if (left != null && left.deleted === true) {
|
||||||
@@ -833,10 +885,7 @@ module.exports = function (Y /* :any */) {
|
|||||||
this.gc2 = this.gc2.filter(filter)
|
this.gc2 = this.gc2.filter(filter)
|
||||||
delete op.gc
|
delete op.gc
|
||||||
}
|
}
|
||||||
* destroy () {
|
destroyTypes () {
|
||||||
clearInterval(this.gcInterval)
|
|
||||||
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) {
|
||||||
@@ -846,6 +895,11 @@ module.exports = function (Y /* :any */) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
* destroy () {
|
||||||
|
clearInterval(this.gcInterval)
|
||||||
|
this.gcInterval = null
|
||||||
|
this.stopRepairCheck()
|
||||||
|
}
|
||||||
setUserId (userId) {
|
setUserId (userId) {
|
||||||
if (!this.userIdPromise.inProgress) {
|
if (!this.userIdPromise.inProgress) {
|
||||||
this.userIdPromise.inProgress = true
|
this.userIdPromise.inProgress = true
|
||||||
@@ -1028,8 +1082,7 @@ module.exports = function (Y /* :any */) {
|
|||||||
*/
|
*/
|
||||||
* operationAdded (transaction, op) {
|
* operationAdded (transaction, op) {
|
||||||
if (op.struct === 'Delete') {
|
if (op.struct === 'Delete') {
|
||||||
var target = yield* transaction.getInsertion(op.target)
|
var type = this.initializedTypes[JSON.stringify(op.targetParent)]
|
||||||
var type = this.initializedTypes[JSON.stringify(target.parent)]
|
|
||||||
if (type != null) {
|
if (type != null) {
|
||||||
yield* type._changed(transaction, op)
|
yield* type._changed(transaction, op)
|
||||||
}
|
}
|
||||||
@@ -1097,10 +1150,8 @@ module.exports = function (Y /* :any */) {
|
|||||||
resolve: resolve,
|
resolve: resolve,
|
||||||
promise: promise
|
promise: promise
|
||||||
}
|
}
|
||||||
return promise
|
|
||||||
} else {
|
|
||||||
return this.transactionsFinished.promise
|
|
||||||
}
|
}
|
||||||
|
return this.transactionsFinished.promise
|
||||||
} else {
|
} else {
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}
|
}
|
||||||
@@ -1216,7 +1267,11 @@ module.exports = function (Y/* :any */) {
|
|||||||
*/
|
*/
|
||||||
Delete: {
|
Delete: {
|
||||||
encode: function (op) {
|
encode: function (op) {
|
||||||
return op
|
return {
|
||||||
|
target: op.target,
|
||||||
|
length: op.length || 0,
|
||||||
|
struct: 'Delete'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
requiredOps: function (op) {
|
requiredOps: function (op) {
|
||||||
return [] // [op.target]
|
return [] // [op.target]
|
||||||
@@ -1694,7 +1749,7 @@ module.exports = function (Y/* :any */) {
|
|||||||
send.push(Y.Struct[op.struct].encode(op))
|
send.push(Y.Struct[op.struct].encode(op))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!this.store.y.connector.isDisconnected() && send.length > 0) { // TODO: && !this.store.forwardAppliedOperations (but then i don't send delete ops)
|
if (this.store.y.connector.isSynced && send.length > 0) { // TODO: && !this.store.forwardAppliedOperations (but then i don't send delete ops)
|
||||||
// is connected, and this is not going to be send in addOperation
|
// is connected, and this is not going to be send in addOperation
|
||||||
this.store.y.connector.broadcastOps(send)
|
this.store.y.connector.broadcastOps(send)
|
||||||
}
|
}
|
||||||
@@ -1804,7 +1859,8 @@ module.exports = function (Y/* :any */) {
|
|||||||
yield* this.store.operationAdded(this, {
|
yield* this.store.operationAdded(this, {
|
||||||
struct: 'Delete',
|
struct: 'Delete',
|
||||||
target: target.id,
|
target: target.id,
|
||||||
length: targetLength
|
length: targetLength,
|
||||||
|
targetParent: target.parent
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// need to gc in the end!
|
// need to gc in the end!
|
||||||
@@ -2311,7 +2367,7 @@ module.exports = function (Y/* :any */) {
|
|||||||
}
|
}
|
||||||
* addOperation (op) {
|
* addOperation (op) {
|
||||||
yield* this.os.put(op)
|
yield* this.os.put(op)
|
||||||
if (!this.store.y.connector.isDisconnected() && this.store.forwardAppliedOperations && typeof op.id[1] !== 'string') {
|
if (this.store.y.connector.isSynced && this.store.forwardAppliedOperations && typeof op.id[1] !== 'string') {
|
||||||
// is connected, and this is not going to be send in addOperation
|
// is connected, and this is not going to be send in addOperation
|
||||||
this.store.y.connector.broadcastOps([op])
|
this.store.y.connector.broadcastOps([op])
|
||||||
}
|
}
|
||||||
@@ -2696,7 +2752,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2726,7 +2782,6 @@ module.exports = function (Y /* : any*/) {
|
|||||||
destroy () {
|
destroy () {
|
||||||
super.destroy()
|
super.destroy()
|
||||||
this.waiting = null
|
this.waiting = null
|
||||||
this.awaiting = null
|
|
||||||
this.onevent = null
|
this.onevent = null
|
||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
@@ -3439,8 +3494,13 @@ Y.extend = function (name, value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Y.requestModules = requestModules
|
Y.requestModules = requestModules
|
||||||
function requestModules (modules, sourceDir) {
|
function requestModules (modules) {
|
||||||
sourceDir = sourceDir || '/bower_components'
|
var sourceDir
|
||||||
|
if (Y.sourceDir === null) {
|
||||||
|
sourceDir = null
|
||||||
|
} else {
|
||||||
|
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..
|
||||||
@@ -3453,10 +3513,11 @@ function requestModules (modules, sourceDir) {
|
|||||||
if (requiringModules[module] == null) {
|
if (requiringModules[module] == null) {
|
||||||
// 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')
|
if (sourceDir != null) {
|
||||||
imported.src = sourceDir + '/' + modulename + '/' + modulename + extention
|
var imported = document.createElement('script')
|
||||||
document.head.appendChild(imported)
|
imported.src = sourceDir + '/' + modulename + '/' + modulename + extention
|
||||||
|
document.head.appendChild(imported)
|
||||||
|
}
|
||||||
let requireModule = {}
|
let requireModule = {}
|
||||||
requiringModules[module] = requireModule
|
requiringModules[module] = requireModule
|
||||||
requireModule.promise = new Promise(function (resolve) {
|
requireModule.promise = new Promise(function (resolve) {
|
||||||
@@ -3505,30 +3566,37 @@ type YOptions = {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
function Y (opts/* :YOptions */) /* :Promise<YConfig> */ {
|
function Y (opts/* :YOptions */) /* :Promise<YConfig> */ {
|
||||||
|
if (opts.hasOwnProperty('sourceDir')) {
|
||||||
|
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])
|
||||||
}
|
}
|
||||||
return new Promise(function (resolve, reject) {
|
return new Promise(function (resolve, reject) {
|
||||||
setTimeout(function () {
|
if (opts == null) reject('An options object is expected! ')
|
||||||
Y.requestModules(modules, opts.sourceDir).then(function () {
|
else if (opts.connector == null) reject('You must specify a connector! (missing connector property)')
|
||||||
if (opts == null) reject('An options object is expected! ')
|
else if (opts.connector.name == null) reject('You must specify connector name! (missing connector.name property)')
|
||||||
else if (opts.connector == null) reject('You must specify a connector! (missing connector property)')
|
else if (opts.db == null) reject('You must specify a database! (missing db 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 db name! (missing db.name property)')
|
||||||
else if (opts.db == null) reject('You must specify a database! (missing db property)')
|
else if (opts.share == null) reject('You must specify a set of shared types!')
|
||||||
else if (opts.connector.name == null) reject('You must specify db name! (missing db.name property)')
|
else {
|
||||||
else if (opts.share == null) reject('You must specify a set of shared types!')
|
opts = Y.utils.copyObject(opts)
|
||||||
else {
|
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)
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3543,6 +3611,7 @@ class YConfig {
|
|||||||
this.options = opts
|
this.options = opts
|
||||||
this.db = new Y[opts.db.name](this, opts.db)
|
this.db = new Y[opts.db.name](this, opts.db)
|
||||||
this.connector = new Y[opts.connector.name](this, opts.connector)
|
this.connector = new Y[opts.connector.name](this, opts.connector)
|
||||||
|
this.connected = true
|
||||||
}
|
}
|
||||||
init (callback) {
|
init (callback) {
|
||||||
var opts = this.options
|
var opts = this.options
|
||||||
@@ -3579,22 +3648,46 @@ class YConfig {
|
|||||||
return this.connector.isSynced
|
return this.connector.isSynced
|
||||||
}
|
}
|
||||||
disconnect () {
|
disconnect () {
|
||||||
return this.connector.disconnect()
|
if (this.connected) {
|
||||||
|
this.connected = false
|
||||||
|
return this.connector.disconnect()
|
||||||
|
} else {
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
reconnect () {
|
reconnect () {
|
||||||
return this.connector.reconnect()
|
if (!this.connected) {
|
||||||
|
this.connected = true
|
||||||
|
return this.connector.reconnect()
|
||||||
|
} else {
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
destroy () {
|
destroy () {
|
||||||
|
var self = this
|
||||||
|
return this.close().then(function () {
|
||||||
|
if (self.db.deleteDB != null) {
|
||||||
|
return self.db.deleteDB()
|
||||||
|
} else {
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
close () {
|
||||||
|
var self = this
|
||||||
|
this.share = null
|
||||||
if (this.connector.destroy != null) {
|
if (this.connector.destroy != null) {
|
||||||
this.connector.destroy()
|
this.connector.destroy()
|
||||||
} else {
|
} else {
|
||||||
this.connector.disconnect()
|
this.connector.disconnect()
|
||||||
}
|
}
|
||||||
var self = this
|
return this.db.whenTransactionsFinished(function () {
|
||||||
this.db.requestTransaction(function * () {
|
this.db.destroyTypes()
|
||||||
yield* self.db.destroy()
|
// make sure to wait for all transactions before destroying the db
|
||||||
self.connector = null
|
this.db.requestTransaction(function * () {
|
||||||
self.db = null
|
yield* self.db.destroy()
|
||||||
|
})
|
||||||
|
return this.db.whenTransactionsFinished()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user