Compare commits
1 Commits
v13.0.0-13
...
v13.0.0-8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
306789ddc1 |
@@ -4,8 +4,8 @@
|
|||||||
<!-- quill does not include dist files! We are using the hosted version instead -->
|
<!-- quill does not include dist files! We are using the hosted version instead -->
|
||||||
<!--link rel="stylesheet" href="../bower_components/quill/dist/quill.snow.css" /-->
|
<!--link rel="stylesheet" href="../bower_components/quill/dist/quill.snow.css" /-->
|
||||||
<link href="https://cdn.quilljs.com/1.0.4/quill.snow.css" rel="stylesheet">
|
<link href="https://cdn.quilljs.com/1.0.4/quill.snow.css" rel="stylesheet">
|
||||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.5.1/katex.min.css" rel="stylesheet">
|
<link href="//cdnjs.cloudflare.com/ajax/libs/KaTeX/0.5.1/katex.min.css" rel="stylesheet">
|
||||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.2.0/styles/monokai-sublime.min.css" rel="stylesheet">
|
<link href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.2.0/styles/monokai-sublime.min.css" rel="stylesheet">
|
||||||
<style>
|
<style>
|
||||||
#quill-container {
|
#quill-container {
|
||||||
border: 1px solid gray;
|
border: 1px solid gray;
|
||||||
@@ -19,17 +19,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.5.1/katex.min.js" type="text/javascript"></script>
|
<script src="//cdnjs.cloudflare.com/ajax/libs/KaTeX/0.5.1/katex.min.js" type="text/javascript"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.2.0/highlight.min.js" type="text/javascript"></script>
|
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.2.0/highlight.min.js" type="text/javascript"></script>
|
||||||
<script src="https://cdn.quilljs.com/1.0.4/quill.js"></script>
|
<script src="https://cdn.quilljs.com/1.0.4/quill.js"></script>
|
||||||
<!-- quill does not include dist files! We are using the hosted version instead (see above)
|
<!-- quill does not include dist files! We are using the hosted version instead (see above)
|
||||||
<script src="../bower_components/quill/dist/quill.js"></script>
|
<script src="../bower_components/quill/dist/quill.js"></script>
|
||||||
-->
|
-->
|
||||||
<script src="../../y.js"></script>
|
<script src="../bower_components/yjs/y.js"></script>
|
||||||
<script src="../../../y-array/y-array.js"></script>
|
|
||||||
<script src="../../../y-richtext/dist/y-richtext.js"></script>
|
|
||||||
<script src="../../../y-memory/y-memory.js"></script>
|
|
||||||
<script src="../../../y-websockets-client/y-websockets-client.js"></script>
|
|
||||||
<script src="./index.js"></script>
|
<script src="./index.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -8,8 +8,7 @@ Y({
|
|||||||
},
|
},
|
||||||
connector: {
|
connector: {
|
||||||
name: 'websockets-client',
|
name: 'websockets-client',
|
||||||
room: 'richtext-example-quill-1.0-test',
|
room: 'richtext-example-quill-1.0-test'
|
||||||
url: 'http://localhost:1234'
|
|
||||||
},
|
},
|
||||||
sourceDir: '/bower_components',
|
sourceDir: '/bower_components',
|
||||||
share: {
|
share: {
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
/* global Y */
|
/* global Y */
|
||||||
|
|
||||||
// eslint-disable-next-line
|
|
||||||
let search = new URLSearchParams(location.search)
|
|
||||||
let url = search.get('url')
|
|
||||||
|
|
||||||
// initialize a shared object. This function call returns a promise!
|
// initialize a shared object. This function call returns a promise!
|
||||||
Y({
|
Y({
|
||||||
db: {
|
db: {
|
||||||
@@ -12,7 +8,7 @@ Y({
|
|||||||
connector: {
|
connector: {
|
||||||
name: 'websockets-client',
|
name: 'websockets-client',
|
||||||
room: 'Textarea-example',
|
room: 'Textarea-example',
|
||||||
url: url || 'http://127.0.0.1:1234'
|
url: 'http://127.0.0.1:1234'
|
||||||
},
|
},
|
||||||
sourceDir: '/bower_components',
|
sourceDir: '/bower_components',
|
||||||
share: {
|
share: {
|
||||||
|
|||||||
2
package-lock.json
generated
2
package-lock.json
generated
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "yjs",
|
"name": "yjs",
|
||||||
"version": "13.0.0-13",
|
"version": "13.0.0-7",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"accepts": {
|
"accepts": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "yjs",
|
"name": "yjs",
|
||||||
"version": "13.0.0-13",
|
"version": "13.0.0-8",
|
||||||
"description": "A framework for real-time p2p shared editing on any data",
|
"description": "A framework for real-time p2p shared editing on any data",
|
||||||
"main": "./y.node.js",
|
"main": "./y.node.js",
|
||||||
"browser": "./y.js",
|
"browser": "./y.js",
|
||||||
|
|||||||
140
src/Connector.js
140
src/Connector.js
@@ -43,12 +43,10 @@ export default function extendConnector (Y/* :any */) {
|
|||||||
this.setUserId(Y.utils.generateUserId())
|
this.setUserId(Y.utils.generateUserId())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reconnect () {
|
reconnect () {
|
||||||
this.log('reconnecting..')
|
this.log('reconnecting..')
|
||||||
return this.y.db.startGarbageCollector()
|
return this.y.db.startGarbageCollector()
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnect () {
|
disconnect () {
|
||||||
this.log('discronnecting..')
|
this.log('discronnecting..')
|
||||||
this.connections = new Map()
|
this.connections = new Map()
|
||||||
@@ -58,16 +56,13 @@ export default function extendConnector (Y/* :any */) {
|
|||||||
this.y.db.stopGarbageCollector()
|
this.y.db.stopGarbageCollector()
|
||||||
return this.y.db.whenTransactionsFinished()
|
return this.y.db.whenTransactionsFinished()
|
||||||
}
|
}
|
||||||
|
|
||||||
repair () {
|
repair () {
|
||||||
this.log('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')
|
this.log('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')
|
||||||
|
this.connections.forEach(user => { user.isSynced = false })
|
||||||
this.isSynced = false
|
this.isSynced = false
|
||||||
this.connections.forEach((user, userId) => {
|
this.currentSyncTarget = null
|
||||||
user.isSynced = false
|
this.findNextSyncTarget()
|
||||||
this._syncWithUser(userId)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setUserId (userId) {
|
setUserId (userId) {
|
||||||
if (this.userId == null) {
|
if (this.userId == null) {
|
||||||
if (!Number.isInteger(userId)) {
|
if (!Number.isInteger(userId)) {
|
||||||
@@ -82,21 +77,20 @@ export default function extendConnector (Y/* :any */) {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onUserEvent (f) {
|
onUserEvent (f) {
|
||||||
this.userEventListeners.push(f)
|
this.userEventListeners.push(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
removeUserEventListener (f) {
|
removeUserEventListener (f) {
|
||||||
this.userEventListeners = this.userEventListeners.filter(g => f !== g)
|
this.userEventListeners = this.userEventListeners.filter(g => f !== g)
|
||||||
}
|
}
|
||||||
|
|
||||||
userLeft (user) {
|
userLeft (user) {
|
||||||
if (this.connections.has(user)) {
|
if (this.connections.has(user)) {
|
||||||
this.log('%s: User left %s', this.userId, user)
|
this.log('%s: User left %s', this.userId, user)
|
||||||
this.connections.delete(user)
|
this.connections.delete(user)
|
||||||
// check if isSynced event can be sent now
|
if (user === this.currentSyncTarget) {
|
||||||
this._setSyncedWith(null)
|
this.currentSyncTarget = null
|
||||||
|
this.findNextSyncTarget()
|
||||||
|
}
|
||||||
for (var f of this.userEventListeners) {
|
for (var f of this.userEventListeners) {
|
||||||
f({
|
f({
|
||||||
action: 'userLeft',
|
action: 'userLeft',
|
||||||
@@ -105,7 +99,7 @@ export default function extendConnector (Y/* :any */) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
userJoined (user, role, auth) {
|
userJoined (user, role) {
|
||||||
if (role == null) {
|
if (role == null) {
|
||||||
throw new Error('You must specify the role of the joined user!')
|
throw new Error('You must specify the role of the joined user!')
|
||||||
}
|
}
|
||||||
@@ -118,7 +112,7 @@ export default function extendConnector (Y/* :any */) {
|
|||||||
isSynced: false,
|
isSynced: false,
|
||||||
role: role,
|
role: role,
|
||||||
processAfterAuth: [],
|
processAfterAuth: [],
|
||||||
auth: auth || null,
|
auth: null,
|
||||||
receivedSyncStep2: false
|
receivedSyncStep2: false
|
||||||
})
|
})
|
||||||
let defer = {}
|
let defer = {}
|
||||||
@@ -131,7 +125,9 @@ export default function extendConnector (Y/* :any */) {
|
|||||||
role: role
|
role: role
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
this._syncWithUser(user)
|
if (this.currentSyncTarget == null) {
|
||||||
|
this.findNextSyncTarget()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Execute a function _when_ we are connected.
|
// Execute a function _when_ we are connected.
|
||||||
// If not connected, wait until connected
|
// If not connected, wait until connected
|
||||||
@@ -142,25 +138,39 @@ export default function extendConnector (Y/* :any */) {
|
|||||||
this.whenSyncedListeners.push(f)
|
this.whenSyncedListeners.push(f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_syncWithUser (userid) {
|
findNextSyncTarget () {
|
||||||
if (this.role === 'slave') {
|
if (this.currentSyncTarget != null || this.role === 'slave') {
|
||||||
return // "The current sync has not finished or this is controlled by a master!"
|
return // "The current sync has not finished or this is controlled by a master!"
|
||||||
}
|
}
|
||||||
sendSyncStep1(this, userid)
|
|
||||||
}
|
var syncUser = null
|
||||||
_fireIsSyncedListeners () {
|
for (var [uid, user] of this.connections) {
|
||||||
this.y.db.whenTransactionsFinished().then(() => {
|
if (!user.isSynced) {
|
||||||
if (!this.isSynced) {
|
syncUser = uid
|
||||||
this.isSynced = true
|
break
|
||||||
// It is safer to remove this!
|
|
||||||
// TODO: remove: yield * this.garbageCollectAfterSync()
|
|
||||||
// call whensynced listeners
|
|
||||||
for (var f of this.whenSyncedListeners) {
|
|
||||||
f()
|
|
||||||
}
|
|
||||||
this.whenSyncedListeners = []
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
var conn = this
|
||||||
|
if (syncUser != null) {
|
||||||
|
this.currentSyncTarget = syncUser
|
||||||
|
sendSyncStep1(this, syncUser)
|
||||||
|
} else {
|
||||||
|
if (!conn.isSynced) {
|
||||||
|
this.y.db.requestTransaction(function * () {
|
||||||
|
if (!conn.isSynced) {
|
||||||
|
// it is crucial that isSynced is set at the time garbageCollectAfterSync is called
|
||||||
|
conn.isSynced = true
|
||||||
|
// It is safer to remove this!
|
||||||
|
// TODO: remove: yield * this.garbageCollectAfterSync()
|
||||||
|
// call whensynced listeners
|
||||||
|
for (var f of conn.whenSyncedListeners) {
|
||||||
|
f()
|
||||||
|
}
|
||||||
|
conn.whenSyncedListeners = []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
send (uid, buffer) {
|
send (uid, buffer) {
|
||||||
if (!(buffer instanceof ArrayBuffer || buffer instanceof Uint8Array)) {
|
if (!(buffer instanceof ArrayBuffer || buffer instanceof Uint8Array)) {
|
||||||
@@ -210,13 +220,12 @@ export default function extendConnector (Y/* :any */) {
|
|||||||
/*
|
/*
|
||||||
You received a raw message, and you know that it is intended for Yjs. Then call this function.
|
You received a raw message, and you know that it is intended for Yjs. Then call this function.
|
||||||
*/
|
*/
|
||||||
receiveMessage (sender, buffer, skipAuth) {
|
async receiveMessage (sender, buffer) {
|
||||||
skipAuth = skipAuth || false
|
|
||||||
if (!(buffer instanceof ArrayBuffer || buffer instanceof Uint8Array)) {
|
if (!(buffer instanceof ArrayBuffer || buffer instanceof Uint8Array)) {
|
||||||
return Promise.reject(new Error('Expected Message to be an ArrayBuffer or Uint8Array!'))
|
throw new Error('Expected Message to be an ArrayBuffer or Uint8Array!')
|
||||||
}
|
}
|
||||||
if (sender === this.userId) {
|
if (sender === this.userId) {
|
||||||
return Promise.resolve()
|
return
|
||||||
}
|
}
|
||||||
let decoder = new BinaryDecoder(buffer)
|
let decoder = new BinaryDecoder(buffer)
|
||||||
let encoder = new BinaryEncoder()
|
let encoder = new BinaryEncoder()
|
||||||
@@ -228,63 +237,60 @@ export default function extendConnector (Y/* :any */) {
|
|||||||
this.log('%s: Receive \'%s\' from %s', this.userId, messageType, sender)
|
this.log('%s: Receive \'%s\' from %s', this.userId, messageType, sender)
|
||||||
this.logMessage('Message: %Y', buffer)
|
this.logMessage('Message: %Y', buffer)
|
||||||
|
|
||||||
if (senderConn == null && !skipAuth) {
|
if (senderConn == null) {
|
||||||
throw new Error('Received message from unknown peer!')
|
throw new Error('Received message from unknown peer!')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (messageType === 'sync step 1' || messageType === 'sync step 2') {
|
if (messageType === 'sync step 1' || messageType === 'sync step 2') {
|
||||||
let auth = decoder.readVarUint()
|
let auth = decoder.readVarUint()
|
||||||
if (senderConn.auth == null) {
|
if (senderConn.auth == null) {
|
||||||
senderConn.processAfterAuth.push([messageType, senderConn, decoder, encoder, sender])
|
|
||||||
// check auth
|
// check auth
|
||||||
return this.checkAuth(auth, this.y, sender).then(authPermissions => {
|
let authPermissions = await this.checkAuth(auth, this.y, sender)
|
||||||
if (senderConn.auth == null) {
|
senderConn.auth = authPermissions
|
||||||
senderConn.auth = authPermissions
|
this.y.emit('userAuthenticated', {
|
||||||
this.y.emit('userAuthenticated', {
|
user: senderConn.uid,
|
||||||
user: senderConn.uid,
|
auth: authPermissions
|
||||||
auth: authPermissions
|
})
|
||||||
})
|
senderConn.syncStep2.promise.then(() => {
|
||||||
|
if (senderConn.processAfterAuth == null) {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
let messages = senderConn.processAfterAuth
|
for (let i = 0; i < senderConn.processAfterAuth.length; i++) {
|
||||||
senderConn.processAfterAuth = []
|
let m = senderConn.processAfterAuth[i]
|
||||||
|
this.receiveMessage(m[0], m[1])
|
||||||
return messages.reduce((p, m) =>
|
}
|
||||||
p.then(() => this.computeMessage(m[0], m[1], m[2], m[3], m[4]))
|
senderConn.processAfterAuth = null
|
||||||
, Promise.resolve())
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (skipAuth || senderConn.auth != null) {
|
|
||||||
return this.computeMessage(messageType, senderConn, decoder, encoder, sender, skipAuth)
|
|
||||||
} else {
|
|
||||||
senderConn.processAfterAuth.push([messageType, senderConn, decoder, encoder, sender, false])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
computeMessage (messageType, senderConn, decoder, encoder, sender, skipAuth) {
|
if (senderConn.auth == null) {
|
||||||
|
senderConn.processAfterAuth.push([sender, buffer])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (messageType === 'sync step 1' && (senderConn.auth === 'write' || senderConn.auth === 'read')) {
|
if (messageType === 'sync step 1' && (senderConn.auth === 'write' || senderConn.auth === 'read')) {
|
||||||
// cannot wait for sync step 1 to finish, because we may wait for sync step 2 in sync step 1 (->lock)
|
// cannot wait for sync step 1 to finish, because we may wait for sync step 2 in sync step 1 (->lock)
|
||||||
computeMessageSyncStep1(decoder, encoder, this, senderConn, sender)
|
computeMessageSyncStep1(decoder, encoder, this, senderConn, sender)
|
||||||
return this.y.db.whenTransactionsFinished()
|
return this.y.db.whenTransactionsFinished()
|
||||||
} else if (messageType === 'sync step 2' && senderConn.auth === 'write') {
|
} else if (messageType === 'sync step 2' && senderConn.auth === 'write') {
|
||||||
return computeMessageSyncStep2(decoder, encoder, this, senderConn, sender)
|
return computeMessageSyncStep2(decoder, encoder, this, senderConn, sender)
|
||||||
} else if (messageType === 'update' && (skipAuth || senderConn.auth === 'write')) {
|
} else if (messageType === 'update' && senderConn.auth === 'write') {
|
||||||
return computeMessageUpdate(decoder, encoder, this, senderConn, sender)
|
return computeMessageUpdate(decoder, encoder, this, senderConn, sender)
|
||||||
} else {
|
} else {
|
||||||
return Promise.reject(new Error('Unable to receive message'))
|
console.error('Unable to receive message')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_setSyncedWith (user) {
|
_setSyncedWith (user) {
|
||||||
if (user != null) {
|
var conn = this.connections.get(user)
|
||||||
this.connections.get(user).isSynced = true
|
if (conn != null) {
|
||||||
|
conn.isSynced = true
|
||||||
}
|
}
|
||||||
let conns = Array.from(this.connections.values())
|
if (user === this.currentSyncTarget) {
|
||||||
if (conns.length > 0 && conns.every(u => u.isSynced)) {
|
this.currentSyncTarget = null
|
||||||
this._fireIsSyncedListeners()
|
this.findNextSyncTarget()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Currently, the HB encodes operations as JSON. For the moment I want to keep it
|
Currently, the HB encodes operations as JSON. For the moment I want to keep it
|
||||||
that way. Maybe we support encoding in the HB as XML in the future, but for now I don't want
|
that way. Maybe we support encoding in the HB as XML in the future, but for now I don't want
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ export default function extendDatabase (Y /* :any */) {
|
|||||||
startGarbageCollector () {
|
startGarbageCollector () {
|
||||||
this.gc = this.dbOpts.gc
|
this.gc = this.dbOpts.gc
|
||||||
if (this.gc) {
|
if (this.gc) {
|
||||||
this.gcTimeout = !this.dbOpts.gcTimeout ? 30000 : this.dbOpts.gcTimeout
|
this.gcTimeout = !this.dbOpts.gcTimeout ? 100000 : this.dbOpts.gcTimeout
|
||||||
} else {
|
} else {
|
||||||
this.gcTimeout = -1
|
this.gcTimeout = -1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,46 +7,37 @@ export class BinaryEncoder {
|
|||||||
constructor () {
|
constructor () {
|
||||||
this.data = []
|
this.data = []
|
||||||
}
|
}
|
||||||
|
|
||||||
get pos () {
|
get pos () {
|
||||||
return this.data.length
|
return this.data.length
|
||||||
}
|
}
|
||||||
|
|
||||||
createBuffer () {
|
createBuffer () {
|
||||||
return Uint8Array.from(this.data).buffer
|
return Uint8Array.from(this.data).buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
writeUint8 (num) {
|
writeUint8 (num) {
|
||||||
this.data.push(num & bits8)
|
this.data.push(num & bits8)
|
||||||
}
|
}
|
||||||
|
|
||||||
setUint8 (pos, num) {
|
setUint8 (pos, num) {
|
||||||
this.data[pos] = num & bits8
|
this.data[pos] = num & bits8
|
||||||
}
|
}
|
||||||
|
|
||||||
writeUint16 (num) {
|
writeUint16 (num) {
|
||||||
this.data.push(num & bits8, (num >>> 8) & bits8)
|
this.data.push(num & bits8, (num >>> 8) & bits8)
|
||||||
}
|
}
|
||||||
|
|
||||||
setUint16 (pos, num) {
|
setUint16 (pos, num) {
|
||||||
this.data[pos] = num & bits8
|
this.data[pos] = num & bits8
|
||||||
this.data[pos + 1] = (num >>> 8) & bits8
|
this.data[pos + 1] = (num >>> 8) & bits8
|
||||||
}
|
}
|
||||||
|
|
||||||
writeUint32 (num) {
|
writeUint32 (num) {
|
||||||
for (let i = 0; i < 4; i++) {
|
for (let i = 0; i < 4; i++) {
|
||||||
this.data.push(num & bits8)
|
this.data.push(num & bits8)
|
||||||
num >>>= 8
|
num >>>= 8
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setUint32 (pos, num) {
|
setUint32 (pos, num) {
|
||||||
for (let i = 0; i < 4; i++) {
|
for (let i = 0; i < 4; i++) {
|
||||||
this.data[pos + i] = num & bits8
|
this.data[pos + i] = num & bits8
|
||||||
num >>>= 8
|
num >>>= 8
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
writeVarUint (num) {
|
writeVarUint (num) {
|
||||||
while (num >= 0b10000000) {
|
while (num >= 0b10000000) {
|
||||||
this.data.push(0b10000000 | (bits7 & num))
|
this.data.push(0b10000000 | (bits7 & num))
|
||||||
@@ -54,7 +45,6 @@ export class BinaryEncoder {
|
|||||||
}
|
}
|
||||||
this.data.push(bits7 & num)
|
this.data.push(bits7 & num)
|
||||||
}
|
}
|
||||||
|
|
||||||
writeVarString (str) {
|
writeVarString (str) {
|
||||||
let bytes = utf8.setBytesFromString(str)
|
let bytes = utf8.setBytesFromString(str)
|
||||||
let len = bytes.length
|
let len = bytes.length
|
||||||
@@ -63,7 +53,6 @@ export class BinaryEncoder {
|
|||||||
this.data.push(bytes[i])
|
this.data.push(bytes[i])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
writeOpID (id) {
|
writeOpID (id) {
|
||||||
let user = id[0]
|
let user = id[0]
|
||||||
this.writeVarUint(user)
|
this.writeVarUint(user)
|
||||||
@@ -79,22 +68,19 @@ export class BinaryDecoder {
|
|||||||
constructor (buffer) {
|
constructor (buffer) {
|
||||||
if (buffer instanceof ArrayBuffer) {
|
if (buffer instanceof ArrayBuffer) {
|
||||||
this.uint8arr = new Uint8Array(buffer)
|
this.uint8arr = new Uint8Array(buffer)
|
||||||
} else if (buffer instanceof Uint8Array || (typeof Buffer !== 'undefined' && buffer instanceof Buffer)) {
|
} else if (buffer instanceof Uint8Array) {
|
||||||
this.uint8arr = buffer
|
this.uint8arr = buffer
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Expected an ArrayBuffer or Uint8Array!')
|
throw new Error('Expected an ArrayBuffer or Uint8Array!')
|
||||||
}
|
}
|
||||||
this.pos = 0
|
this.pos = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
skip8 () {
|
skip8 () {
|
||||||
this.pos++
|
this.pos++
|
||||||
}
|
}
|
||||||
|
|
||||||
readUint8 () {
|
readUint8 () {
|
||||||
return this.uint8arr[this.pos++]
|
return this.uint8arr[this.pos++]
|
||||||
}
|
}
|
||||||
|
|
||||||
readUint32 () {
|
readUint32 () {
|
||||||
let uint =
|
let uint =
|
||||||
this.uint8arr[this.pos] +
|
this.uint8arr[this.pos] +
|
||||||
@@ -104,11 +90,9 @@ export class BinaryDecoder {
|
|||||||
this.pos += 4
|
this.pos += 4
|
||||||
return uint
|
return uint
|
||||||
}
|
}
|
||||||
|
|
||||||
peekUint8 () {
|
peekUint8 () {
|
||||||
return this.uint8arr[this.pos]
|
return this.uint8arr[this.pos]
|
||||||
}
|
}
|
||||||
|
|
||||||
readVarUint () {
|
readVarUint () {
|
||||||
let num = 0
|
let num = 0
|
||||||
let len = 0
|
let len = 0
|
||||||
@@ -124,7 +108,6 @@ export class BinaryDecoder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
readVarString () {
|
readVarString () {
|
||||||
let len = this.readVarUint()
|
let len = this.readVarUint()
|
||||||
let bytes = new Array(len)
|
let bytes = new Array(len)
|
||||||
@@ -133,14 +116,12 @@ export class BinaryDecoder {
|
|||||||
}
|
}
|
||||||
return utf8.getStringFromBytes(bytes)
|
return utf8.getStringFromBytes(bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
peekVarString () {
|
peekVarString () {
|
||||||
let pos = this.pos
|
let pos = this.pos
|
||||||
let s = this.readVarString()
|
let s = this.readVarString()
|
||||||
this.pos = pos
|
this.pos = pos
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
readOpID () {
|
readOpID () {
|
||||||
let user = this.readVarUint()
|
let user = this.readVarUint()
|
||||||
if (user !== 0xFFFFFF) {
|
if (user !== 0xFFFFFF) {
|
||||||
|
|||||||
@@ -26,15 +26,15 @@ export function formatYjsMessageType (buffer) {
|
|||||||
return decoder.readVarString()
|
return decoder.readVarString()
|
||||||
}
|
}
|
||||||
|
|
||||||
export function logMessageUpdate (decoder, strBuilder) {
|
export async function logMessageUpdate (decoder, strBuilder) {
|
||||||
let len = decoder.readUint32()
|
let len = decoder.readUint32()
|
||||||
for (let i = 0; i < len; i++) {
|
for (let i = 0; i < len; i++) {
|
||||||
strBuilder.push(JSON.stringify(Y.Struct.binaryDecodeOperation(decoder)) + '\n')
|
strBuilder.push(JSON.stringify(Y.Struct.binaryDecodeOperation(decoder)) + '\n')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function computeMessageUpdate (decoder, encoder, conn) {
|
export async function computeMessageUpdate (decoder, encoder, conn) {
|
||||||
if (conn.y.db.forwardAppliedOperations || conn.y.persistence != null) {
|
if (conn.y.db.forwardAppliedOperations) {
|
||||||
let messagePosition = decoder.pos
|
let messagePosition = decoder.pos
|
||||||
let len = decoder.readUint32()
|
let len = decoder.readUint32()
|
||||||
let delops = []
|
let delops = []
|
||||||
@@ -45,12 +45,7 @@ export function computeMessageUpdate (decoder, encoder, conn) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (delops.length > 0) {
|
if (delops.length > 0) {
|
||||||
if (conn.y.db.forwardAppliedOperations) {
|
conn.broadcastOps(delops)
|
||||||
conn.broadcastOps(delops)
|
|
||||||
}
|
|
||||||
if (conn.y.persistence) {
|
|
||||||
conn.y.persistence.saveOperations(delops)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
decoder.pos = messagePosition
|
decoder.pos = messagePosition
|
||||||
}
|
}
|
||||||
@@ -83,7 +78,7 @@ export function logMessageSyncStep1 (decoder, strBuilder) {
|
|||||||
logSS(decoder, strBuilder)
|
logSS(decoder, strBuilder)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function computeMessageSyncStep1 (decoder, encoder, conn, senderConn, sender) {
|
export async function computeMessageSyncStep1 (decoder, encoder, conn, senderConn, sender) {
|
||||||
let protocolVersion = decoder.readVarUint()
|
let protocolVersion = decoder.readVarUint()
|
||||||
let preferUntransformed = decoder.readUint8() === 1
|
let preferUntransformed = decoder.readUint8() === 1
|
||||||
|
|
||||||
@@ -97,30 +92,27 @@ export function computeMessageSyncStep1 (decoder, encoder, conn, senderConn, sen
|
|||||||
conn.y.destroy()
|
conn.y.destroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
return conn.y.db.whenTransactionsFinished().then(() => {
|
// send sync step 2
|
||||||
// send sync step 2
|
conn.y.db.requestTransaction(function * () {
|
||||||
conn.y.db.requestTransaction(function * () {
|
encoder.writeVarString('sync step 2')
|
||||||
encoder.writeVarString('sync step 2')
|
encoder.writeVarString(conn.authInfo || '')
|
||||||
encoder.writeVarString(conn.authInfo || '')
|
|
||||||
|
|
||||||
if (preferUntransformed) {
|
if (preferUntransformed) {
|
||||||
encoder.writeUint8(1)
|
encoder.writeUint8(1)
|
||||||
yield * this.writeOperationsUntransformed(encoder)
|
yield * this.writeOperationsUntransformed(encoder)
|
||||||
} else {
|
} else {
|
||||||
encoder.writeUint8(0)
|
encoder.writeUint8(0)
|
||||||
yield * this.writeOperations(encoder, decoder)
|
yield * this.writeOperations(encoder, decoder)
|
||||||
}
|
}
|
||||||
|
|
||||||
yield * this.writeDeleteSet(encoder)
|
yield * this.writeDeleteSet(encoder)
|
||||||
conn.send(senderConn.uid, encoder.createBuffer())
|
conn.send(senderConn.uid, encoder.createBuffer())
|
||||||
senderConn.receivedSyncStep2 = true
|
senderConn.receivedSyncStep2 = true
|
||||||
})
|
|
||||||
return conn.y.db.whenTransactionsFinished().then(() => {
|
|
||||||
if (conn.role === 'slave') {
|
|
||||||
sendSyncStep1(conn, sender)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
if (conn.role === 'slave') {
|
||||||
|
sendSyncStep1(conn, sender)
|
||||||
|
}
|
||||||
|
await conn.y.db.whenTransactionsFinished()
|
||||||
}
|
}
|
||||||
|
|
||||||
export function logSS (decoder, strBuilder) {
|
export function logSS (decoder, strBuilder) {
|
||||||
@@ -169,7 +161,7 @@ export function logMessageSyncStep2 (decoder, strBuilder) {
|
|||||||
logDS(decoder, strBuilder)
|
logDS(decoder, strBuilder)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function computeMessageSyncStep2 (decoder, encoder, conn, senderConn, sender) {
|
export async function computeMessageSyncStep2 (decoder, encoder, conn, senderConn, sender) {
|
||||||
var db = conn.y.db
|
var db = conn.y.db
|
||||||
let defer = senderConn.syncStep2
|
let defer = senderConn.syncStep2
|
||||||
|
|
||||||
@@ -186,8 +178,7 @@ export function computeMessageSyncStep2 (decoder, encoder, conn, senderConn, sen
|
|||||||
db.requestTransaction(function * () {
|
db.requestTransaction(function * () {
|
||||||
yield * this.applyDeleteSet(decoder)
|
yield * this.applyDeleteSet(decoder)
|
||||||
})
|
})
|
||||||
return db.whenTransactionsFinished().then(() => {
|
await db.whenTransactionsFinished()
|
||||||
conn._setSyncedWith(sender)
|
conn._setSyncedWith(sender)
|
||||||
defer.resolve()
|
defer.resolve()
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
import { BinaryEncoder } from './Encoding.js'
|
|
||||||
|
|
||||||
export default function extendPersistence (Y) {
|
|
||||||
class AbstractPersistence {
|
|
||||||
constructor (y, opts) {
|
|
||||||
this.y = y
|
|
||||||
this.opts = opts
|
|
||||||
this.saveOperationsBuffer = []
|
|
||||||
this.log = Y.debug('y:persistence')
|
|
||||||
}
|
|
||||||
saveToMessageQueue (binary) {
|
|
||||||
this.log('Room %s: Save message to message queue', this.y.options.connector.room)
|
|
||||||
}
|
|
||||||
saveOperations (ops) {
|
|
||||||
ops = ops.map(function (op) {
|
|
||||||
return Y.Struct[op.struct].encode(op)
|
|
||||||
})
|
|
||||||
const saveOperations = () => {
|
|
||||||
if (this.saveOperationsBuffer.length > 0) {
|
|
||||||
let encoder = new BinaryEncoder()
|
|
||||||
encoder.writeVarString(this.opts.room)
|
|
||||||
encoder.writeVarString('update')
|
|
||||||
let ops = this.saveOperationsBuffer
|
|
||||||
this.saveOperationsBuffer = []
|
|
||||||
let length = ops.length
|
|
||||||
encoder.writeUint32(length)
|
|
||||||
for (var i = 0; i < length; i++) {
|
|
||||||
let op = ops[i]
|
|
||||||
Y.Struct[op.struct].binaryEncode(encoder, op)
|
|
||||||
}
|
|
||||||
this.saveToMessageQueue(encoder.createBuffer())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (this.saveOperationsBuffer.length === 0) {
|
|
||||||
this.saveOperationsBuffer = ops
|
|
||||||
this.y.db.whenTransactionsFinished().then(saveOperations)
|
|
||||||
} else {
|
|
||||||
this.saveOperationsBuffer = this.saveOperationsBuffer.concat(ops)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Y.AbstractPersistence = AbstractPersistence
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { BinaryEncoder, BinaryDecoder } from './Encoding.js'
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Partial definition of a transaction
|
Partial definition of a transaction
|
||||||
@@ -98,9 +97,6 @@ export default function extendTransaction (Y) {
|
|||||||
if (send.length > 0) { // TODO: && !this.store.forwardAppliedOperations (but then i don't send delete ops)
|
if (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)
|
||||||
if (this.store.y.persistence != null) {
|
|
||||||
this.store.y.persistence.saveOperations(send)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -683,15 +679,10 @@ export default function extendTransaction (Y) {
|
|||||||
yield * this.garbageCollectOperation(o.id)
|
yield * this.garbageCollectOperation(o.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.store.forwardAppliedOperations || this.store.y.persistence != null) {
|
if (this.store.forwardAppliedOperations) {
|
||||||
var ops = []
|
var ops = []
|
||||||
ops.push({struct: 'Delete', target: [del[0], del[1]], length: del[2]})
|
ops.push({struct: 'Delete', target: [del[0], del[1]], length: del[2]})
|
||||||
if (this.store.forwardAppliedOperations) {
|
this.store.y.connector.broadcastOps(ops)
|
||||||
this.store.y.connector.broadcastOps(ops)
|
|
||||||
}
|
|
||||||
if (this.store.y.persistence != null) {
|
|
||||||
this.store.y.persistence.saveOperations(ops)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -742,14 +733,9 @@ export default function extendTransaction (Y) {
|
|||||||
* addOperation (op) {
|
* addOperation (op) {
|
||||||
yield * this.os.put(op)
|
yield * this.os.put(op)
|
||||||
// case op is created by this user, op is already broadcasted in applyCreatedOperations
|
// case op is created by this user, op is already broadcasted in applyCreatedOperations
|
||||||
if (op.id[0] !== this.store.userId && typeof op.id[1] !== 'string') {
|
if (op.id[0] !== this.store.userId && this.store.forwardAppliedOperations && typeof op.id[1] !== 'string') {
|
||||||
if (this.store.forwardAppliedOperations) {
|
// 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])
|
|
||||||
}
|
|
||||||
if (this.store.y.persistence != null) {
|
|
||||||
this.store.y.persistence.saveOperations([op])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// if insertion, try to combine with left insertion (if both have content property)
|
// if insertion, try to combine with left insertion (if both have content property)
|
||||||
@@ -1086,20 +1072,6 @@ export default function extendTransaction (Y) {
|
|||||||
Y.Struct[op.struct].binaryEncode(encoder, Y.Struct[op.struct].encode(op))
|
Y.Struct[op.struct].binaryEncode(encoder, Y.Struct[op.struct].encode(op))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
* toBinary () {
|
|
||||||
let encoder = new BinaryEncoder()
|
|
||||||
yield * this.writeOperationsUntransformed(encoder)
|
|
||||||
yield * this.writeDeleteSet(encoder)
|
|
||||||
return encoder.createBuffer()
|
|
||||||
}
|
|
||||||
|
|
||||||
* fromBinary (buffer) {
|
|
||||||
let decoder = new BinaryDecoder(buffer)
|
|
||||||
yield * this.applyOperationsUntransformed(decoder)
|
|
||||||
yield * this.applyDeleteSet(decoder)
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Get the plain untransformed operations from the database.
|
* Get the plain untransformed operations from the database.
|
||||||
* You can apply these operations using .applyOperationsUntransformed(ops)
|
* You can apply these operations using .applyOperationsUntransformed(ops)
|
||||||
|
|||||||
17
src/y.js
17
src/y.js
@@ -1,5 +1,4 @@
|
|||||||
import extendConnector from './Connector.js'
|
import extendConnector from './Connector.js'
|
||||||
import extendPersistence from './Persistence.js'
|
|
||||||
import extendDatabase from './Database.js'
|
import extendDatabase from './Database.js'
|
||||||
import extendTransaction from './Transaction.js'
|
import extendTransaction from './Transaction.js'
|
||||||
import extendStruct from './Struct.js'
|
import extendStruct from './Struct.js'
|
||||||
@@ -8,7 +7,6 @@ import debug from 'debug'
|
|||||||
import { formatYjsMessage, formatYjsMessageType } from './MessageHandler.js'
|
import { formatYjsMessage, formatYjsMessageType } from './MessageHandler.js'
|
||||||
|
|
||||||
extendConnector(Y)
|
extendConnector(Y)
|
||||||
extendPersistence(Y)
|
|
||||||
extendDatabase(Y)
|
extendDatabase(Y)
|
||||||
extendTransaction(Y)
|
extendTransaction(Y)
|
||||||
extendStruct(Y)
|
extendStruct(Y)
|
||||||
@@ -161,11 +159,6 @@ class YConfig extends Y.utils.NamedEventHandler {
|
|||||||
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)
|
||||||
if (opts.persistence != null) {
|
|
||||||
this.persistence = new Y[opts.persistence.name](this, opts.persistence)
|
|
||||||
} else {
|
|
||||||
this.persistence = null
|
|
||||||
}
|
|
||||||
this.connected = true
|
this.connected = true
|
||||||
}
|
}
|
||||||
init (callback) {
|
init (callback) {
|
||||||
@@ -195,15 +188,9 @@ class YConfig extends Y.utils.NamedEventHandler {
|
|||||||
}
|
}
|
||||||
share[propertyname] = yield * this.store.initType.call(this, id, args)
|
share[propertyname] = yield * this.store.initType.call(this, id, args)
|
||||||
}
|
}
|
||||||
|
this.store.whenTransactionsFinished()
|
||||||
|
.then(callback)
|
||||||
})
|
})
|
||||||
if (this.persistence != null) {
|
|
||||||
this.persistence.retrieveContent()
|
|
||||||
.then(() => this.db.whenTransactionsFinished())
|
|
||||||
.then(callback)
|
|
||||||
} else {
|
|
||||||
this.db.whenTransactionsFinished()
|
|
||||||
.then(callback)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
isConnected () {
|
isConnected () {
|
||||||
return this.connector.isSynced
|
return this.connector.isSynced
|
||||||
|
|||||||
@@ -1,374 +0,0 @@
|
|||||||
import { wait, initArrays, compareUsers, Y, flushAll, garbageCollectUsers, applyRandomTests } from '../tests-lib/helper.js'
|
|
||||||
import { test, proxyConsole } from 'cutest'
|
|
||||||
|
|
||||||
proxyConsole()
|
|
||||||
|
|
||||||
test('basic spec', async function array0 (t) {
|
|
||||||
let { users, array0 } = await initArrays(t, { users: 2 })
|
|
||||||
|
|
||||||
array0.delete(0, 0)
|
|
||||||
t.assert(true, 'Does not throw when deleting zero elements with position 0')
|
|
||||||
|
|
||||||
let throwInvalidPosition = false
|
|
||||||
try {
|
|
||||||
array0.delete(1, 0)
|
|
||||||
} catch (e) {
|
|
||||||
throwInvalidPosition = true
|
|
||||||
}
|
|
||||||
t.assert(throwInvalidPosition, 'Throws when deleting zero elements with an invalid position')
|
|
||||||
|
|
||||||
array0.insert(0, ['A'])
|
|
||||||
array0.delete(1, 0)
|
|
||||||
t.assert(true, 'Does not throw when deleting zero elements with valid position 1')
|
|
||||||
|
|
||||||
await compareUsers(t, users)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('insert three elements, try re-get property', async function array1 (t) {
|
|
||||||
var { users, array0, array1 } = await initArrays(t, { users: 2 })
|
|
||||||
array0.insert(0, [1, 2, 3])
|
|
||||||
t.compare(array0.toArray(), [1, 2, 3], '.toArray() works')
|
|
||||||
await flushAll(t, users)
|
|
||||||
t.compare(array1.toArray(), [1, 2, 3], '.toArray() works after sync')
|
|
||||||
await compareUsers(t, users)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('concurrent insert (handle three conflicts)', async function array2 (t) {
|
|
||||||
var { users, array0, array1, array2 } = await initArrays(t, { users: 3 })
|
|
||||||
array0.insert(0, [0])
|
|
||||||
array1.insert(0, [1])
|
|
||||||
array2.insert(0, [2])
|
|
||||||
|
|
||||||
await compareUsers(t, users)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('concurrent insert&delete (handle three conflicts)', async function array3 (t) {
|
|
||||||
var { users, array0, array1, array2 } = await initArrays(t, { users: 3 })
|
|
||||||
array0.insert(0, ['x', 'y', 'z'])
|
|
||||||
await flushAll(t, users)
|
|
||||||
array0.insert(1, [0])
|
|
||||||
array1.delete(0)
|
|
||||||
array1.delete(1, 1)
|
|
||||||
array2.insert(1, [2])
|
|
||||||
|
|
||||||
await compareUsers(t, users)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('insertions work in late sync', async function array4 (t) {
|
|
||||||
var { users, array0, array1, array2 } = await initArrays(t, { users: 3 })
|
|
||||||
array0.insert(0, ['x', 'y'])
|
|
||||||
await flushAll(t, users)
|
|
||||||
users[1].disconnect()
|
|
||||||
users[2].disconnect()
|
|
||||||
array0.insert(1, ['user0'])
|
|
||||||
array1.insert(1, ['user1'])
|
|
||||||
array2.insert(1, ['user2'])
|
|
||||||
await users[1].reconnect()
|
|
||||||
await users[2].reconnect()
|
|
||||||
await compareUsers(t, users)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('disconnect really prevents sending messages', async function array5 (t) {
|
|
||||||
var { users, array0, array1 } = await initArrays(t, { users: 3 })
|
|
||||||
array0.insert(0, ['x', 'y'])
|
|
||||||
await flushAll(t, users)
|
|
||||||
users[1].disconnect()
|
|
||||||
users[2].disconnect()
|
|
||||||
array0.insert(1, ['user0'])
|
|
||||||
array1.insert(1, ['user1'])
|
|
||||||
await wait(1000)
|
|
||||||
t.compare(array0.toArray(), ['x', 'user0', 'y'])
|
|
||||||
t.compare(array1.toArray(), ['x', 'user1', 'y'])
|
|
||||||
await users[1].reconnect()
|
|
||||||
await users[2].reconnect()
|
|
||||||
await compareUsers(t, users)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('deletions in late sync', async function array6 (t) {
|
|
||||||
var { users, array0, array1 } = await initArrays(t, { users: 2 })
|
|
||||||
array0.insert(0, ['x', 'y'])
|
|
||||||
await flushAll(t, users)
|
|
||||||
await users[1].disconnect()
|
|
||||||
array1.delete(1, 1)
|
|
||||||
array0.delete(0, 2)
|
|
||||||
await wait()
|
|
||||||
await users[1].reconnect()
|
|
||||||
await compareUsers(t, users)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('insert, then marge delete on sync', async function array7 (t) {
|
|
||||||
var { users, array0, array1 } = await initArrays(t, { users: 2 })
|
|
||||||
array0.insert(0, ['x', 'y', 'z'])
|
|
||||||
await flushAll(t, users)
|
|
||||||
await wait()
|
|
||||||
await users[0].disconnect()
|
|
||||||
array1.delete(0, 3)
|
|
||||||
await wait()
|
|
||||||
await users[0].reconnect()
|
|
||||||
await compareUsers(t, users)
|
|
||||||
})
|
|
||||||
|
|
||||||
function compareEvent (t, is, should) {
|
|
||||||
for (var key in should) {
|
|
||||||
t.assert(
|
|
||||||
should[key] === is[key] ||
|
|
||||||
JSON.stringify(should[key]) === JSON.stringify(is[key])
|
|
||||||
, 'event works as expected'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
test('insert & delete events', async function array8 (t) {
|
|
||||||
var { array0, users } = await initArrays(t, { users: 2 })
|
|
||||||
var event
|
|
||||||
array0.observe(function (e) {
|
|
||||||
event = e
|
|
||||||
})
|
|
||||||
array0.insert(0, [0, 1, 2])
|
|
||||||
compareEvent(t, event, {
|
|
||||||
type: 'insert',
|
|
||||||
index: 0,
|
|
||||||
values: [0, 1, 2],
|
|
||||||
length: 3
|
|
||||||
})
|
|
||||||
array0.delete(0)
|
|
||||||
compareEvent(t, event, {
|
|
||||||
type: 'delete',
|
|
||||||
index: 0,
|
|
||||||
length: 1,
|
|
||||||
values: [0]
|
|
||||||
})
|
|
||||||
array0.delete(0, 2)
|
|
||||||
compareEvent(t, event, {
|
|
||||||
type: 'delete',
|
|
||||||
index: 0,
|
|
||||||
length: 2,
|
|
||||||
values: [1, 2]
|
|
||||||
})
|
|
||||||
await compareUsers(t, users)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('insert & delete events for types', async function array9 (t) {
|
|
||||||
var { array0, users } = await initArrays(t, { users: 2 })
|
|
||||||
var event
|
|
||||||
array0.observe(function (e) {
|
|
||||||
event = e
|
|
||||||
})
|
|
||||||
array0.insert(0, [Y.Array])
|
|
||||||
compareEvent(t, event, {
|
|
||||||
type: 'insert',
|
|
||||||
object: array0,
|
|
||||||
index: 0,
|
|
||||||
length: 1
|
|
||||||
})
|
|
||||||
var type = array0.get(0)
|
|
||||||
t.assert(type._model != null, 'Model of type is defined')
|
|
||||||
array0.delete(0)
|
|
||||||
compareEvent(t, event, {
|
|
||||||
type: 'delete',
|
|
||||||
object: array0,
|
|
||||||
index: 0,
|
|
||||||
length: 1
|
|
||||||
})
|
|
||||||
await compareUsers(t, users)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('insert & delete events for types (2)', async function array10 (t) {
|
|
||||||
var { array0, users } = await initArrays(t, { users: 2 })
|
|
||||||
var events = []
|
|
||||||
array0.observe(function (e) {
|
|
||||||
events.push(e)
|
|
||||||
})
|
|
||||||
array0.insert(0, ['hi', Y.Map])
|
|
||||||
compareEvent(t, events[0], {
|
|
||||||
type: 'insert',
|
|
||||||
object: array0,
|
|
||||||
index: 0,
|
|
||||||
length: 1,
|
|
||||||
values: ['hi']
|
|
||||||
})
|
|
||||||
compareEvent(t, events[1], {
|
|
||||||
type: 'insert',
|
|
||||||
object: array0,
|
|
||||||
index: 1,
|
|
||||||
length: 1
|
|
||||||
})
|
|
||||||
array0.delete(1)
|
|
||||||
compareEvent(t, events[2], {
|
|
||||||
type: 'delete',
|
|
||||||
object: array0,
|
|
||||||
index: 1,
|
|
||||||
length: 1
|
|
||||||
})
|
|
||||||
await compareUsers(t, users)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('garbage collector', async function gc1 (t) {
|
|
||||||
var { users, array0 } = await initArrays(t, { users: 3 })
|
|
||||||
|
|
||||||
array0.insert(0, ['x', 'y', 'z'])
|
|
||||||
await flushAll(t, users)
|
|
||||||
users[0].disconnect()
|
|
||||||
array0.delete(0, 3)
|
|
||||||
await wait()
|
|
||||||
await users[0].reconnect()
|
|
||||||
await flushAll(t, users)
|
|
||||||
await garbageCollectUsers(t, users)
|
|
||||||
await compareUsers(t, users)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('event has correct value when setting a primitive on a YArray (same user)', async function array11 (t) {
|
|
||||||
var { array0, users } = await initArrays(t, { users: 3 })
|
|
||||||
|
|
||||||
var event
|
|
||||||
array0.observe(function (e) {
|
|
||||||
event = e
|
|
||||||
})
|
|
||||||
array0.insert(0, ['stuff'])
|
|
||||||
t.assert(event.values[0] === event.object.get(0), 'compare value with get method')
|
|
||||||
t.assert(event.values[0] === 'stuff', 'check that value is actually present')
|
|
||||||
t.assert(event.values[0] === array0.toArray()[0], '.toArray works as expected')
|
|
||||||
await compareUsers(t, users)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('event has correct value when setting a primitive on a YArray (received from another user)', async function array12 (t) {
|
|
||||||
var { users, array0, array1 } = await initArrays(t, { users: 3 })
|
|
||||||
|
|
||||||
var event
|
|
||||||
array0.observe(function (e) {
|
|
||||||
event = e
|
|
||||||
})
|
|
||||||
array1.insert(0, ['stuff'])
|
|
||||||
await flushAll(t, users)
|
|
||||||
t.assert(event.values[0] === event.object.get(0), 'compare value with get method')
|
|
||||||
t.assert(event.values[0] === 'stuff', 'check that value is actually present')
|
|
||||||
t.assert(event.values[0] === array0.toArray()[0], '.toArray works as expected')
|
|
||||||
await compareUsers(t, users)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('event has correct value when setting a type on a YArray (same user)', async function array13 (t) {
|
|
||||||
var { array0, users } = await initArrays(t, { users: 3 })
|
|
||||||
|
|
||||||
var event
|
|
||||||
array0.observe(function (e) {
|
|
||||||
event = e
|
|
||||||
})
|
|
||||||
array0.insert(0, [Y.Array])
|
|
||||||
t.assert(event.values[0] === event.object.get(0), 'compare value with get method')
|
|
||||||
t.assert(event.values[0] != null, 'event.value exists')
|
|
||||||
t.assert(event.values[0] === array0.toArray()[0], '.toArray works as expected')
|
|
||||||
await compareUsers(t, users)
|
|
||||||
})
|
|
||||||
test('event has correct value when setting a type on a YArray (ops received from another user)', async function array14 (t) {
|
|
||||||
var { users, array0, array1 } = await initArrays(t, { users: 3 })
|
|
||||||
|
|
||||||
var event
|
|
||||||
array0.observe(function (e) {
|
|
||||||
event = e
|
|
||||||
})
|
|
||||||
array1.insert(0, [Y.Array])
|
|
||||||
await flushAll(t, users)
|
|
||||||
t.assert(event.values[0] === event.object.get(0), 'compare value with get method')
|
|
||||||
t.assert(event.values[0] != null, 'event.value exists')
|
|
||||||
t.assert(event.values[0] === array0.toArray()[0], '.toArray works as expected')
|
|
||||||
await compareUsers(t, users)
|
|
||||||
})
|
|
||||||
|
|
||||||
var _uniqueNumber = 0
|
|
||||||
function getUniqueNumber () {
|
|
||||||
return _uniqueNumber++
|
|
||||||
}
|
|
||||||
|
|
||||||
var arrayTransactions = [
|
|
||||||
function insert (t, user, chance) {
|
|
||||||
var uniqueNumber = getUniqueNumber()
|
|
||||||
var content = []
|
|
||||||
var len = chance.integer({ min: 1, max: 4 })
|
|
||||||
for (var i = 0; i < len; i++) {
|
|
||||||
content.push(uniqueNumber)
|
|
||||||
}
|
|
||||||
var pos = chance.integer({ min: 0, max: user.share.array.length })
|
|
||||||
user.share.array.insert(pos, content)
|
|
||||||
},
|
|
||||||
function insertTypeArray (t, user, chance) {
|
|
||||||
var pos = chance.integer({ min: 0, max: user.share.array.length })
|
|
||||||
user.share.array.insert(pos, [Y.Array])
|
|
||||||
var array2 = user.share.array.get(pos)
|
|
||||||
array2.insert(0, [1, 2, 3, 4])
|
|
||||||
},
|
|
||||||
function insertTypeMap (t, user, chance) {
|
|
||||||
var pos = chance.integer({ min: 0, max: user.share.array.length })
|
|
||||||
user.share.array.insert(pos, [Y.Map])
|
|
||||||
var map = user.share.array.get(pos)
|
|
||||||
map.set('someprop', 42)
|
|
||||||
map.set('someprop', 43)
|
|
||||||
map.set('someprop', 44)
|
|
||||||
},
|
|
||||||
function _delete (t, user, chance) {
|
|
||||||
var length = user.share.array._content.length
|
|
||||||
if (length > 0) {
|
|
||||||
var pos = chance.integer({ min: 0, max: length - 1 })
|
|
||||||
var delLength = chance.integer({ min: 1, max: Math.min(2, length - pos) })
|
|
||||||
if (user.share.array._content[pos].type != null) {
|
|
||||||
if (chance.bool()) {
|
|
||||||
var type = user.share.array.get(pos)
|
|
||||||
if (type instanceof Y.Array.typeDefinition.class) {
|
|
||||||
if (type._content.length > 0) {
|
|
||||||
pos = chance.integer({ min: 0, max: type._content.length - 1 })
|
|
||||||
delLength = chance.integer({ min: 0, max: Math.min(2, type._content.length - pos) })
|
|
||||||
type.delete(pos, delLength)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
type.delete('someprop')
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
user.share.array.delete(pos, delLength)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
user.share.array.delete(pos, delLength)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
test('y-array: Random tests (42)', async function randomArray42 (t) {
|
|
||||||
await applyRandomTests(t, arrayTransactions, 42)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('y-array: Random tests (43)', async function randomArray43 (t) {
|
|
||||||
await applyRandomTests(t, arrayTransactions, 43)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('y-array: Random tests (44)', async function randomArray44 (t) {
|
|
||||||
await applyRandomTests(t, arrayTransactions, 44)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('y-array: Random tests (45)', async function randomArray45 (t) {
|
|
||||||
await applyRandomTests(t, arrayTransactions, 45)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('y-array: Random tests (46)', async function randomArray46 (t) {
|
|
||||||
await applyRandomTests(t, arrayTransactions, 46)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('y-array: Random tests (47)', async function randomArray47 (t) {
|
|
||||||
await applyRandomTests(t, arrayTransactions, 47)
|
|
||||||
})
|
|
||||||
|
|
||||||
/*
|
|
||||||
test('y-array: Random tests (200)', async function randomArray200 (t) {
|
|
||||||
await applyRandomTests(t, arrayTransactions, 200)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('y-array: Random tests (300)', async function randomArray300 (t) {
|
|
||||||
await applyRandomTests(t, arrayTransactions, 300)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('y-array: Random tests (400)', async function randomArray400 (t) {
|
|
||||||
await applyRandomTests(t, arrayTransactions, 400)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('y-array: Random tests (500)', async function randomArray500 (t) {
|
|
||||||
await applyRandomTests(t, arrayTransactions, 500)
|
|
||||||
})
|
|
||||||
*/
|
|
||||||
@@ -1,371 +0,0 @@
|
|||||||
import { initArrays, compareUsers, Y, flushAll, applyRandomTests } from '../tests-lib/helper.js'
|
|
||||||
import { test, proxyConsole } from 'cutest'
|
|
||||||
|
|
||||||
proxyConsole()
|
|
||||||
|
|
||||||
test('basic map tests', async function map0 (t) {
|
|
||||||
let { users, map0, map1, map2 } = await initArrays(t, { users: 3 })
|
|
||||||
users[2].disconnect()
|
|
||||||
|
|
||||||
map0.set('number', 1)
|
|
||||||
map0.set('string', 'hello Y')
|
|
||||||
map0.set('object', { key: { key2: 'value' } })
|
|
||||||
map0.set('y-map', Y.Map)
|
|
||||||
let map = map0.get('y-map')
|
|
||||||
map.set('y-array', Y.Array)
|
|
||||||
let array = map.get('y-array')
|
|
||||||
array.insert(0, [0])
|
|
||||||
array.insert(0, [-1])
|
|
||||||
|
|
||||||
t.assert(map0.get('number') === 1, 'client 0 computed the change (number)')
|
|
||||||
t.assert(map0.get('string') === 'hello Y', 'client 0 computed the change (string)')
|
|
||||||
t.compare(map0.get('object'), { key: { key2: 'value' } }, 'client 0 computed the change (object)')
|
|
||||||
t.assert(map0.get('y-map').get('y-array').get(0) === -1, 'client 0 computed the change (type)')
|
|
||||||
|
|
||||||
await users[2].reconnect()
|
|
||||||
await flushAll(t, users)
|
|
||||||
|
|
||||||
t.assert(map1.get('number') === 1, 'client 1 received the update (number)')
|
|
||||||
t.assert(map1.get('string') === 'hello Y', 'client 1 received the update (string)')
|
|
||||||
t.compare(map1.get('object'), { key: { key2: 'value' } }, 'client 1 received the update (object)')
|
|
||||||
t.assert(map1.get('y-map').get('y-array').get(0) === -1, 'client 1 received the update (type)')
|
|
||||||
|
|
||||||
// compare disconnected user
|
|
||||||
t.assert(map2.get('number') === 1, 'client 2 received the update (number) - was disconnected')
|
|
||||||
t.assert(map2.get('string') === 'hello Y', 'client 2 received the update (string) - was disconnected')
|
|
||||||
t.compare(map2.get('object'), { key: { key2: 'value' } }, 'client 2 received the update (object) - was disconnected')
|
|
||||||
t.assert(map2.get('y-map').get('y-array').get(0) === -1, 'client 2 received the update (type) - was disconnected')
|
|
||||||
await compareUsers(t, users)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('Basic get&set of Map property (converge via sync)', async function map1 (t) {
|
|
||||||
let { users, map0 } = await initArrays(t, { users: 2 })
|
|
||||||
map0.set('stuff', 'stuffy')
|
|
||||||
t.compare(map0.get('stuff'), 'stuffy')
|
|
||||||
|
|
||||||
await flushAll(t, users)
|
|
||||||
|
|
||||||
for (let user of users) {
|
|
||||||
var u = user.share.map
|
|
||||||
t.compare(u.get('stuff'), 'stuffy')
|
|
||||||
}
|
|
||||||
await compareUsers(t, users)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('Map can set custom types (Map)', async function map2 (t) {
|
|
||||||
let { users, map0 } = await initArrays(t, { users: 2 })
|
|
||||||
var map = map0.set('Map', Y.Map)
|
|
||||||
map.set('one', 1)
|
|
||||||
map = map0.get('Map')
|
|
||||||
t.compare(map.get('one'), 1)
|
|
||||||
await compareUsers(t, users)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('Map can set custom types (Map) - get also returns the type', async function map3 (t) {
|
|
||||||
let { users, map0 } = await initArrays(t, { users: 2 })
|
|
||||||
map0.set('Map', Y.Map)
|
|
||||||
var map = map0.get('Map')
|
|
||||||
map.set('one', 1)
|
|
||||||
map = map0.get('Map')
|
|
||||||
t.compare(map.get('one'), 1)
|
|
||||||
await compareUsers(t, users)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('Map can set custom types (Array)', async function map4 (t) {
|
|
||||||
let { users, map0 } = await initArrays(t, { users: 2 })
|
|
||||||
var array = map0.set('Array', Y.Array)
|
|
||||||
array.insert(0, [1, 2, 3])
|
|
||||||
array = map0.get('Array')
|
|
||||||
t.compare(array.toArray(), [1, 2, 3])
|
|
||||||
await compareUsers(t, users)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('Basic get&set of Map property (converge via update)', async function map5 (t) {
|
|
||||||
let { users, map0 } = await initArrays(t, { users: 2 })
|
|
||||||
map0.set('stuff', 'stuffy')
|
|
||||||
t.compare(map0.get('stuff'), 'stuffy')
|
|
||||||
|
|
||||||
await flushAll(t, users)
|
|
||||||
|
|
||||||
for (let user of users) {
|
|
||||||
var u = user.share.map
|
|
||||||
t.compare(u.get('stuff'), 'stuffy')
|
|
||||||
}
|
|
||||||
await compareUsers(t, users)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('Basic get&set of Map property (handle conflict)', async function map6 (t) {
|
|
||||||
let { users, map0, map1 } = await initArrays(t, { users: 3 })
|
|
||||||
map0.set('stuff', 'c0')
|
|
||||||
map1.set('stuff', 'c1')
|
|
||||||
|
|
||||||
await flushAll(t, users)
|
|
||||||
|
|
||||||
for (let user of users) {
|
|
||||||
var u = user.share.map
|
|
||||||
t.compare(u.get('stuff'), 'c0')
|
|
||||||
}
|
|
||||||
await compareUsers(t, users)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('Basic get&set&delete of Map property (handle conflict)', async function map7 (t) {
|
|
||||||
let { users, map0, map1 } = await initArrays(t, { users: 3 })
|
|
||||||
map0.set('stuff', 'c0')
|
|
||||||
map0.delete('stuff')
|
|
||||||
map1.set('stuff', 'c1')
|
|
||||||
await flushAll(t, users)
|
|
||||||
for (let user of users) {
|
|
||||||
var u = user.share.map
|
|
||||||
t.assert(u.get('stuff') === undefined)
|
|
||||||
}
|
|
||||||
await compareUsers(t, users)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('Basic get&set of Map property (handle three conflicts)', async function map8 (t) {
|
|
||||||
let { users, map0, map1, map2 } = await initArrays(t, { users: 3 })
|
|
||||||
map0.set('stuff', 'c0')
|
|
||||||
map1.set('stuff', 'c1')
|
|
||||||
map1.set('stuff', 'c2')
|
|
||||||
map2.set('stuff', 'c3')
|
|
||||||
await flushAll(t, users)
|
|
||||||
for (let user of users) {
|
|
||||||
var u = user.share.map
|
|
||||||
t.compare(u.get('stuff'), 'c0')
|
|
||||||
}
|
|
||||||
await compareUsers(t, users)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('Basic get&set&delete of Map property (handle three conflicts)', async function map9 (t) {
|
|
||||||
let { users, map0, map1, map2, map3 } = await initArrays(t, { users: 4 })
|
|
||||||
map0.set('stuff', 'c0')
|
|
||||||
map1.set('stuff', 'c1')
|
|
||||||
map1.set('stuff', 'c2')
|
|
||||||
map2.set('stuff', 'c3')
|
|
||||||
await flushAll(t, users)
|
|
||||||
map0.set('stuff', 'deleteme')
|
|
||||||
map0.delete('stuff')
|
|
||||||
map1.set('stuff', 'c1')
|
|
||||||
map2.set('stuff', 'c2')
|
|
||||||
map3.set('stuff', 'c3')
|
|
||||||
await flushAll(t, users)
|
|
||||||
for (let user of users) {
|
|
||||||
var u = user.share.map
|
|
||||||
t.assert(u.get('stuff') === undefined)
|
|
||||||
}
|
|
||||||
await compareUsers(t, users)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('observePath properties', async function map10 (t) {
|
|
||||||
let { users, map0, map1, map2 } = await initArrays(t, { users: 3 })
|
|
||||||
let map
|
|
||||||
map0.observePath(['map'], function (map) {
|
|
||||||
if (map != null) {
|
|
||||||
map.set('yay', 4)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
map1.set('map', Y.Map)
|
|
||||||
await flushAll(t, users)
|
|
||||||
map = map2.get('map')
|
|
||||||
t.compare(map.get('yay'), 4)
|
|
||||||
await compareUsers(t, users)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('observe deep properties', async function map11 (t) {
|
|
||||||
let { users, map1, map2, map3 } = await initArrays(t, { users: 4 })
|
|
||||||
var _map1 = map1.set('map', Y.Map)
|
|
||||||
var calls = 0
|
|
||||||
var dmapid
|
|
||||||
_map1.observe(function (event) {
|
|
||||||
calls++
|
|
||||||
t.compare(event.name, 'deepmap')
|
|
||||||
dmapid = event.object.opContents.deepmap
|
|
||||||
})
|
|
||||||
await flushAll(t, users)
|
|
||||||
var _map3 = map3.get('map')
|
|
||||||
_map3.set('deepmap', Y.Map)
|
|
||||||
await flushAll(t, users)
|
|
||||||
var _map2 = map2.get('map')
|
|
||||||
_map2.set('deepmap', Y.Map)
|
|
||||||
await flushAll(t, users)
|
|
||||||
var dmap1 = _map1.get('deepmap')
|
|
||||||
var dmap2 = _map2.get('deepmap')
|
|
||||||
var dmap3 = _map3.get('deepmap')
|
|
||||||
t.assert(calls > 0)
|
|
||||||
t.compare(dmap1._model, dmap2._model)
|
|
||||||
t.compare(dmap1._model, dmap3._model)
|
|
||||||
t.compare(dmap1._model, dmapid)
|
|
||||||
await compareUsers(t, users)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('observes using observePath', async function map12 (t) {
|
|
||||||
let { users, map0 } = await initArrays(t, { users: 2 })
|
|
||||||
var pathes = []
|
|
||||||
var calls = 0
|
|
||||||
map0.observeDeep(function (event) {
|
|
||||||
pathes.push(event.path)
|
|
||||||
calls++
|
|
||||||
})
|
|
||||||
map0.set('map', Y.Map)
|
|
||||||
map0.get('map').set('array', Y.Array)
|
|
||||||
map0.get('map').get('array').insert(0, ['content'])
|
|
||||||
t.assert(calls === 3)
|
|
||||||
t.compare(pathes, [[], ['map'], ['map', 'array']])
|
|
||||||
await compareUsers(t, users)
|
|
||||||
})
|
|
||||||
|
|
||||||
function compareEvent (t, is, should) {
|
|
||||||
for (var key in should) {
|
|
||||||
t.assert(should[key] === is[key])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
test('throws add & update & delete events (with type and primitive content)', async function map13 (t) {
|
|
||||||
let { users, map0 } = await initArrays(t, { users: 2 })
|
|
||||||
var event
|
|
||||||
await flushAll(t, users)
|
|
||||||
map0.observe(function (e) {
|
|
||||||
event = e // just put it on event, should be thrown synchronously anyway
|
|
||||||
})
|
|
||||||
map0.set('stuff', 4)
|
|
||||||
compareEvent(t, event, {
|
|
||||||
type: 'add',
|
|
||||||
object: map0,
|
|
||||||
name: 'stuff'
|
|
||||||
})
|
|
||||||
// update, oldValue is in contents
|
|
||||||
map0.set('stuff', Y.Array)
|
|
||||||
compareEvent(t, event, {
|
|
||||||
type: 'update',
|
|
||||||
object: map0,
|
|
||||||
name: 'stuff',
|
|
||||||
oldValue: 4
|
|
||||||
})
|
|
||||||
var replacedArray = map0.get('stuff')
|
|
||||||
// update, oldValue is in opContents
|
|
||||||
map0.set('stuff', 5)
|
|
||||||
var array = event.oldValue
|
|
||||||
t.compare(array._model, replacedArray._model)
|
|
||||||
// delete
|
|
||||||
map0.delete('stuff')
|
|
||||||
compareEvent(t, event, {
|
|
||||||
type: 'delete',
|
|
||||||
name: 'stuff',
|
|
||||||
object: map0,
|
|
||||||
oldValue: 5
|
|
||||||
})
|
|
||||||
await compareUsers(t, users)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('event has correct value when setting a primitive on a YMap (same user)', async function map14 (t) {
|
|
||||||
let { users, map0 } = await initArrays(t, { users: 3 })
|
|
||||||
var event
|
|
||||||
await flushAll(t, users)
|
|
||||||
map0.observe(function (e) {
|
|
||||||
event = e
|
|
||||||
})
|
|
||||||
map0.set('stuff', 2)
|
|
||||||
t.compare(event.value, event.object.get(event.name))
|
|
||||||
await compareUsers(t, users)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('event has correct value when setting a primitive on a YMap (received from another user)', async function map15 (t) {
|
|
||||||
let { users, map0, map1 } = await initArrays(t, { users: 3 })
|
|
||||||
var event
|
|
||||||
await flushAll(t, users)
|
|
||||||
map0.observe(function (e) {
|
|
||||||
event = e
|
|
||||||
})
|
|
||||||
map1.set('stuff', 2)
|
|
||||||
await flushAll(t, users)
|
|
||||||
t.compare(event.value, event.object.get(event.name))
|
|
||||||
await compareUsers(t, users)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('event has correct value when setting a type on a YMap (same user)', async function map16 (t) {
|
|
||||||
let { users, map0 } = await initArrays(t, { users: 3 })
|
|
||||||
var event
|
|
||||||
await flushAll(t, users)
|
|
||||||
map0.observe(function (e) {
|
|
||||||
event = e
|
|
||||||
})
|
|
||||||
map0.set('stuff', Y.Map)
|
|
||||||
t.compare(event.value._model, event.object.get(event.name)._model)
|
|
||||||
await compareUsers(t, users)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('event has correct value when setting a type on a YMap (ops received from another user)', async function map17 (t) {
|
|
||||||
let { users, map0, map1 } = await initArrays(t, { users: 3 })
|
|
||||||
var event
|
|
||||||
await flushAll(t, users)
|
|
||||||
map0.observe(function (e) {
|
|
||||||
event = e
|
|
||||||
})
|
|
||||||
map1.set('stuff', Y.Map)
|
|
||||||
await flushAll(t, users)
|
|
||||||
t.compare(event.value._model, event.object.get(event.name)._model)
|
|
||||||
await compareUsers(t, users)
|
|
||||||
})
|
|
||||||
|
|
||||||
var mapTransactions = [
|
|
||||||
function set (t, user, chance) {
|
|
||||||
let key = chance.pickone(['one', 'two'])
|
|
||||||
var value = chance.string()
|
|
||||||
user.share.map.set(key, value)
|
|
||||||
},
|
|
||||||
function setType (t, user, chance) {
|
|
||||||
let key = chance.pickone(['one', 'two'])
|
|
||||||
var value = chance.pickone([Y.Array, Y.Map])
|
|
||||||
let type = user.share.map.set(key, value)
|
|
||||||
if (value === Y.Array) {
|
|
||||||
type.insert(0, [1, 2, 3, 4])
|
|
||||||
} else {
|
|
||||||
type.set('deepkey', 'deepvalue')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
function _delete (t, user, chance) {
|
|
||||||
let key = chance.pickone(['one', 'two'])
|
|
||||||
user.share.map.delete(key)
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
test('y-map: Random tests (42)', async function randomMap42 (t) {
|
|
||||||
await applyRandomTests(t, mapTransactions, 42)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('y-map: Random tests (43)', async function randomMap43 (t) {
|
|
||||||
await applyRandomTests(t, mapTransactions, 43)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('y-map: Random tests (44)', async function randomMap44 (t) {
|
|
||||||
await applyRandomTests(t, mapTransactions, 44)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('y-map: Random tests (45)', async function randomMap45 (t) {
|
|
||||||
await applyRandomTests(t, mapTransactions, 45)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('y-map: Random tests (46)', async function randomMap46 (t) {
|
|
||||||
await applyRandomTests(t, mapTransactions, 46)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('y-map: Random tests (47)', async function randomMap47 (t) {
|
|
||||||
await applyRandomTests(t, mapTransactions, 47)
|
|
||||||
})
|
|
||||||
|
|
||||||
/*
|
|
||||||
test('y-map: Random tests (200)', async function randomMap200 (t) {
|
|
||||||
await applyRandomTests(t, mapTransactions, 200)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('y-map: Random tests (300)', async function randomMap300 (t) {
|
|
||||||
await applyRandomTests(t, mapTransactions, 300)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('y-map: Random tests (400)', async function randomMap400 (t) {
|
|
||||||
await applyRandomTests(t, mapTransactions, 400)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('y-map: Random tests (500)', async function randomMap500 (t) {
|
|
||||||
await applyRandomTests(t, mapTransactions, 500)
|
|
||||||
})
|
|
||||||
*/
|
|
||||||
@@ -12,9 +12,6 @@ export let Y = _Y
|
|||||||
|
|
||||||
Y.extend(yMemory, yArray, yMap, yTest)
|
Y.extend(yMemory, yArray, yMap, yTest)
|
||||||
|
|
||||||
export var database = { name: 'memory' }
|
|
||||||
export var connector = { name: 'test', url: 'http://localhost:1234' }
|
|
||||||
|
|
||||||
function * getStateSet () {
|
function * getStateSet () {
|
||||||
var ss = {}
|
var ss = {}
|
||||||
yield * this.ss.iterate(this, null, null, function * (n) {
|
yield * this.ss.iterate(this, null, null, function * (n) {
|
||||||
@@ -63,16 +60,7 @@ export async function compareUsers (t, users) {
|
|||||||
await wait()
|
await wait()
|
||||||
await flushAll(t, users)
|
await flushAll(t, users)
|
||||||
|
|
||||||
var userArrayValues = users.map(u => u.share.array._content.map(c => c.val || JSON.stringify(c.type)))
|
var userTypeContents = users.map(u => u.share.array._content.map(c => c.val || JSON.stringify(c.type)))
|
||||||
function valueToComparable (v) {
|
|
||||||
if (v != null && v._model != null) {
|
|
||||||
return v._model
|
|
||||||
} else {
|
|
||||||
return v || null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var userMapOneValues = users.map(u => u.share.map.get('one')).map(valueToComparable)
|
|
||||||
var userMapTwoValues = users.map(u => u.share.map.get('two')).map(valueToComparable)
|
|
||||||
|
|
||||||
await users[0].db.garbageCollect()
|
await users[0].db.garbageCollect()
|
||||||
await users[0].db.garbageCollect()
|
await users[0].db.garbageCollect()
|
||||||
@@ -130,9 +118,7 @@ export async function compareUsers (t, users) {
|
|||||||
}))
|
}))
|
||||||
for (var i = 0; i < data.length - 1; i++) {
|
for (var i = 0; i < data.length - 1; i++) {
|
||||||
await t.asyncGroup(async () => {
|
await t.asyncGroup(async () => {
|
||||||
t.compare(userArrayValues[i], userArrayValues[i + 1], 'array types')
|
t.compare(userTypeContents[i], userTypeContents[i + 1], 'types')
|
||||||
t.compare(userMapOneValues[i], userMapOneValues[i + 1], 'map types (propery "one")')
|
|
||||||
t.compare(userMapTwoValues[i], userMapTwoValues[i + 1], 'map types (propery "two")')
|
|
||||||
t.compare(data[i].os, data[i + 1].os, 'os')
|
t.compare(data[i].os, data[i + 1].os, 'os')
|
||||||
t.compare(data[i].ds, data[i + 1].ds, 'ds')
|
t.compare(data[i].ds, data[i + 1].ds, 'ds')
|
||||||
t.compare(data[i].ss, data[i + 1].ss, 'ss')
|
t.compare(data[i].ss, data[i + 1].ss, 'ss')
|
||||||
@@ -149,17 +135,17 @@ export async function initArrays (t, opts) {
|
|||||||
}
|
}
|
||||||
var share = Object.assign({ flushHelper: 'Map', array: 'Array', map: 'Map' }, opts.share)
|
var share = Object.assign({ flushHelper: 'Map', array: 'Array', map: 'Map' }, opts.share)
|
||||||
var chance = opts.chance || new Chance(t.getSeed() * 1000000000)
|
var chance = opts.chance || new Chance(t.getSeed() * 1000000000)
|
||||||
var conn = Object.assign({ room: 'debugging_' + t.name, generateUserId: false, testContext: t, chance }, connector)
|
var connector = Object.assign({ room: 'debugging_' + t.name, generateUserId: false, testContext: t, chance }, opts.connector)
|
||||||
for (let i = 0; i < opts.users; i++) {
|
for (let i = 0; i < opts.users; i++) {
|
||||||
let dbOpts
|
let dbOpts
|
||||||
let connOpts
|
let connOpts
|
||||||
if (i === 0) {
|
if (i === 0) {
|
||||||
// Only one instance can gc!
|
// Only one instance can gc!
|
||||||
dbOpts = Object.assign({ gc: false }, database)
|
dbOpts = Object.assign({ gc: true }, opts.db)
|
||||||
connOpts = Object.assign({ role: 'master' }, conn)
|
connOpts = Object.assign({ role: 'master' }, connector)
|
||||||
} else {
|
} else {
|
||||||
dbOpts = Object.assign({ gc: false }, database)
|
dbOpts = Object.assign({ gc: false }, opts.db)
|
||||||
connOpts = Object.assign({ role: 'slave' }, conn)
|
connOpts = Object.assign({ role: 'slave' }, connector)
|
||||||
}
|
}
|
||||||
let y = await Y({
|
let y = await Y({
|
||||||
connector: connOpts,
|
connector: connOpts,
|
||||||
@@ -234,48 +220,3 @@ export function wait (t) {
|
|||||||
setTimeout(resolve, t != null ? t : 100)
|
setTimeout(resolve, t != null ? t : 100)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function applyRandomTests (t, mods, iterations) {
|
|
||||||
const chance = new Chance(t.getSeed() * 1000000000)
|
|
||||||
var initInformation = await initArrays(t, { users: 5, chance: chance })
|
|
||||||
let { users } = initInformation
|
|
||||||
for (var i = 0; i < iterations; i++) {
|
|
||||||
if (chance.bool({likelihood: 10})) {
|
|
||||||
// 10% chance to disconnect/reconnect a user
|
|
||||||
// we make sure that the first users always is connected
|
|
||||||
let user = chance.pickone(users.slice(1))
|
|
||||||
if (user.connector.isSynced) {
|
|
||||||
if (users.filter(u => u.connector.isSynced).length > 1) {
|
|
||||||
// make sure that at least one user remains in the room
|
|
||||||
await user.disconnect()
|
|
||||||
if (users[0].connector.testRoom == null) {
|
|
||||||
await wait(100)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await user.reconnect()
|
|
||||||
if (users[0].connector.testRoom == null) {
|
|
||||||
await wait(100)
|
|
||||||
}
|
|
||||||
await new Promise(function (resolve) {
|
|
||||||
user.connector.whenSynced(resolve)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else if (chance.bool({likelihood: 5})) {
|
|
||||||
// 20%*!prev chance to flush all & garbagecollect
|
|
||||||
// TODO: We do not gc all users as this does not work yet
|
|
||||||
// await garbageCollectUsers(t, users)
|
|
||||||
await flushAll(t, users)
|
|
||||||
await users[0].db.emptyGarbageCollector()
|
|
||||||
await flushAll(t, users)
|
|
||||||
} else if (chance.bool({likelihood: 10})) {
|
|
||||||
// 20%*!prev chance to flush some operations
|
|
||||||
await flushSome(t, users)
|
|
||||||
}
|
|
||||||
let user = chance.pickone(users)
|
|
||||||
var test = chance.pickone(mods)
|
|
||||||
test(t, user, chance)
|
|
||||||
}
|
|
||||||
await compareUsers(t, users)
|
|
||||||
return initInformation
|
|
||||||
}
|
|
||||||
|
|||||||
327
y.node.js
327
y.node.js
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* yjs - A framework for real-time p2p shared editing on any data
|
* yjs - A framework for real-time p2p shared editing on any data
|
||||||
* @version v13.0.0-13
|
* @version v13.0.0-8
|
||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -291,46 +291,37 @@ class BinaryEncoder {
|
|||||||
constructor () {
|
constructor () {
|
||||||
this.data = [];
|
this.data = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
get pos () {
|
get pos () {
|
||||||
return this.data.length
|
return this.data.length
|
||||||
}
|
}
|
||||||
|
|
||||||
createBuffer () {
|
createBuffer () {
|
||||||
return Uint8Array.from(this.data).buffer
|
return Uint8Array.from(this.data).buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
writeUint8 (num) {
|
writeUint8 (num) {
|
||||||
this.data.push(num & bits8);
|
this.data.push(num & bits8);
|
||||||
}
|
}
|
||||||
|
|
||||||
setUint8 (pos, num) {
|
setUint8 (pos, num) {
|
||||||
this.data[pos] = num & bits8;
|
this.data[pos] = num & bits8;
|
||||||
}
|
}
|
||||||
|
|
||||||
writeUint16 (num) {
|
writeUint16 (num) {
|
||||||
this.data.push(num & bits8, (num >>> 8) & bits8);
|
this.data.push(num & bits8, (num >>> 8) & bits8);
|
||||||
}
|
}
|
||||||
|
|
||||||
setUint16 (pos, num) {
|
setUint16 (pos, num) {
|
||||||
this.data[pos] = num & bits8;
|
this.data[pos] = num & bits8;
|
||||||
this.data[pos + 1] = (num >>> 8) & bits8;
|
this.data[pos + 1] = (num >>> 8) & bits8;
|
||||||
}
|
}
|
||||||
|
|
||||||
writeUint32 (num) {
|
writeUint32 (num) {
|
||||||
for (let i = 0; i < 4; i++) {
|
for (let i = 0; i < 4; i++) {
|
||||||
this.data.push(num & bits8);
|
this.data.push(num & bits8);
|
||||||
num >>>= 8;
|
num >>>= 8;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setUint32 (pos, num) {
|
setUint32 (pos, num) {
|
||||||
for (let i = 0; i < 4; i++) {
|
for (let i = 0; i < 4; i++) {
|
||||||
this.data[pos + i] = num & bits8;
|
this.data[pos + i] = num & bits8;
|
||||||
num >>>= 8;
|
num >>>= 8;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
writeVarUint (num) {
|
writeVarUint (num) {
|
||||||
while (num >= 0b10000000) {
|
while (num >= 0b10000000) {
|
||||||
this.data.push(0b10000000 | (bits7 & num));
|
this.data.push(0b10000000 | (bits7 & num));
|
||||||
@@ -338,7 +329,6 @@ class BinaryEncoder {
|
|||||||
}
|
}
|
||||||
this.data.push(bits7 & num);
|
this.data.push(bits7 & num);
|
||||||
}
|
}
|
||||||
|
|
||||||
writeVarString (str) {
|
writeVarString (str) {
|
||||||
let bytes = UTF8_1.setBytesFromString(str);
|
let bytes = UTF8_1.setBytesFromString(str);
|
||||||
let len = bytes.length;
|
let len = bytes.length;
|
||||||
@@ -347,7 +337,6 @@ class BinaryEncoder {
|
|||||||
this.data.push(bytes[i]);
|
this.data.push(bytes[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
writeOpID (id) {
|
writeOpID (id) {
|
||||||
let user = id[0];
|
let user = id[0];
|
||||||
this.writeVarUint(user);
|
this.writeVarUint(user);
|
||||||
@@ -363,22 +352,19 @@ class BinaryDecoder {
|
|||||||
constructor (buffer) {
|
constructor (buffer) {
|
||||||
if (buffer instanceof ArrayBuffer) {
|
if (buffer instanceof ArrayBuffer) {
|
||||||
this.uint8arr = new Uint8Array(buffer);
|
this.uint8arr = new Uint8Array(buffer);
|
||||||
} else if (buffer instanceof Uint8Array || (typeof Buffer !== 'undefined' && buffer instanceof Buffer)) {
|
} else if (buffer instanceof Uint8Array) {
|
||||||
this.uint8arr = buffer;
|
this.uint8arr = buffer;
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Expected an ArrayBuffer or Uint8Array!')
|
throw new Error('Expected an ArrayBuffer or Uint8Array!')
|
||||||
}
|
}
|
||||||
this.pos = 0;
|
this.pos = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
skip8 () {
|
skip8 () {
|
||||||
this.pos++;
|
this.pos++;
|
||||||
}
|
}
|
||||||
|
|
||||||
readUint8 () {
|
readUint8 () {
|
||||||
return this.uint8arr[this.pos++]
|
return this.uint8arr[this.pos++]
|
||||||
}
|
}
|
||||||
|
|
||||||
readUint32 () {
|
readUint32 () {
|
||||||
let uint =
|
let uint =
|
||||||
this.uint8arr[this.pos] +
|
this.uint8arr[this.pos] +
|
||||||
@@ -388,11 +374,9 @@ class BinaryDecoder {
|
|||||||
this.pos += 4;
|
this.pos += 4;
|
||||||
return uint
|
return uint
|
||||||
}
|
}
|
||||||
|
|
||||||
peekUint8 () {
|
peekUint8 () {
|
||||||
return this.uint8arr[this.pos]
|
return this.uint8arr[this.pos]
|
||||||
}
|
}
|
||||||
|
|
||||||
readVarUint () {
|
readVarUint () {
|
||||||
let num = 0;
|
let num = 0;
|
||||||
let len = 0;
|
let len = 0;
|
||||||
@@ -408,7 +392,6 @@ class BinaryDecoder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
readVarString () {
|
readVarString () {
|
||||||
let len = this.readVarUint();
|
let len = this.readVarUint();
|
||||||
let bytes = new Array(len);
|
let bytes = new Array(len);
|
||||||
@@ -417,14 +400,12 @@ class BinaryDecoder {
|
|||||||
}
|
}
|
||||||
return UTF8_1.getStringFromBytes(bytes)
|
return UTF8_1.getStringFromBytes(bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
peekVarString () {
|
peekVarString () {
|
||||||
let pos = this.pos;
|
let pos = this.pos;
|
||||||
let s = this.readVarString();
|
let s = this.readVarString();
|
||||||
this.pos = pos;
|
this.pos = pos;
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
readOpID () {
|
readOpID () {
|
||||||
let user = this.readVarUint();
|
let user = this.readVarUint();
|
||||||
if (user !== 0xFFFFFF) {
|
if (user !== 0xFFFFFF) {
|
||||||
@@ -459,15 +440,15 @@ function formatYjsMessageType (buffer) {
|
|||||||
return decoder.readVarString()
|
return decoder.readVarString()
|
||||||
}
|
}
|
||||||
|
|
||||||
function logMessageUpdate (decoder, strBuilder) {
|
async function logMessageUpdate (decoder, strBuilder) {
|
||||||
let len = decoder.readUint32();
|
let len = decoder.readUint32();
|
||||||
for (let i = 0; i < len; i++) {
|
for (let i = 0; i < len; i++) {
|
||||||
strBuilder.push(JSON.stringify(Y.Struct.binaryDecodeOperation(decoder)) + '\n');
|
strBuilder.push(JSON.stringify(Y.Struct.binaryDecodeOperation(decoder)) + '\n');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function computeMessageUpdate (decoder, encoder, conn) {
|
async function computeMessageUpdate (decoder, encoder, conn) {
|
||||||
if (conn.y.db.forwardAppliedOperations || conn.y.persistence != null) {
|
if (conn.y.db.forwardAppliedOperations) {
|
||||||
let messagePosition = decoder.pos;
|
let messagePosition = decoder.pos;
|
||||||
let len = decoder.readUint32();
|
let len = decoder.readUint32();
|
||||||
let delops = [];
|
let delops = [];
|
||||||
@@ -478,12 +459,7 @@ function computeMessageUpdate (decoder, encoder, conn) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (delops.length > 0) {
|
if (delops.length > 0) {
|
||||||
if (conn.y.db.forwardAppliedOperations) {
|
conn.broadcastOps(delops);
|
||||||
conn.broadcastOps(delops);
|
|
||||||
}
|
|
||||||
if (conn.y.persistence) {
|
|
||||||
conn.y.persistence.saveOperations(delops);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
decoder.pos = messagePosition;
|
decoder.pos = messagePosition;
|
||||||
}
|
}
|
||||||
@@ -516,7 +492,7 @@ function logMessageSyncStep1 (decoder, strBuilder) {
|
|||||||
logSS(decoder, strBuilder);
|
logSS(decoder, strBuilder);
|
||||||
}
|
}
|
||||||
|
|
||||||
function computeMessageSyncStep1 (decoder, encoder, conn, senderConn, sender) {
|
async function computeMessageSyncStep1 (decoder, encoder, conn, senderConn, sender) {
|
||||||
let protocolVersion = decoder.readVarUint();
|
let protocolVersion = decoder.readVarUint();
|
||||||
let preferUntransformed = decoder.readUint8() === 1;
|
let preferUntransformed = decoder.readUint8() === 1;
|
||||||
|
|
||||||
@@ -530,30 +506,27 @@ function computeMessageSyncStep1 (decoder, encoder, conn, senderConn, sender) {
|
|||||||
conn.y.destroy();
|
conn.y.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
return conn.y.db.whenTransactionsFinished().then(() => {
|
// send sync step 2
|
||||||
// send sync step 2
|
conn.y.db.requestTransaction(function * () {
|
||||||
conn.y.db.requestTransaction(function * () {
|
encoder.writeVarString('sync step 2');
|
||||||
encoder.writeVarString('sync step 2');
|
encoder.writeVarString(conn.authInfo || '');
|
||||||
encoder.writeVarString(conn.authInfo || '');
|
|
||||||
|
|
||||||
if (preferUntransformed) {
|
if (preferUntransformed) {
|
||||||
encoder.writeUint8(1);
|
encoder.writeUint8(1);
|
||||||
yield * this.writeOperationsUntransformed(encoder);
|
yield * this.writeOperationsUntransformed(encoder);
|
||||||
} else {
|
} else {
|
||||||
encoder.writeUint8(0);
|
encoder.writeUint8(0);
|
||||||
yield * this.writeOperations(encoder, decoder);
|
yield * this.writeOperations(encoder, decoder);
|
||||||
}
|
}
|
||||||
|
|
||||||
yield * this.writeDeleteSet(encoder);
|
yield * this.writeDeleteSet(encoder);
|
||||||
conn.send(senderConn.uid, encoder.createBuffer());
|
conn.send(senderConn.uid, encoder.createBuffer());
|
||||||
senderConn.receivedSyncStep2 = true;
|
senderConn.receivedSyncStep2 = true;
|
||||||
});
|
});
|
||||||
return conn.y.db.whenTransactionsFinished().then(() => {
|
if (conn.role === 'slave') {
|
||||||
if (conn.role === 'slave') {
|
sendSyncStep1(conn, sender);
|
||||||
sendSyncStep1(conn, sender);
|
}
|
||||||
}
|
await conn.y.db.whenTransactionsFinished();
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function logSS (decoder, strBuilder) {
|
function logSS (decoder, strBuilder) {
|
||||||
@@ -602,7 +575,7 @@ function logMessageSyncStep2 (decoder, strBuilder) {
|
|||||||
logDS(decoder, strBuilder);
|
logDS(decoder, strBuilder);
|
||||||
}
|
}
|
||||||
|
|
||||||
function computeMessageSyncStep2 (decoder, encoder, conn, senderConn, sender) {
|
async function computeMessageSyncStep2 (decoder, encoder, conn, senderConn, sender) {
|
||||||
var db = conn.y.db;
|
var db = conn.y.db;
|
||||||
let defer = senderConn.syncStep2;
|
let defer = senderConn.syncStep2;
|
||||||
|
|
||||||
@@ -619,10 +592,9 @@ function computeMessageSyncStep2 (decoder, encoder, conn, senderConn, sender) {
|
|||||||
db.requestTransaction(function * () {
|
db.requestTransaction(function * () {
|
||||||
yield * this.applyDeleteSet(decoder);
|
yield * this.applyDeleteSet(decoder);
|
||||||
});
|
});
|
||||||
return db.whenTransactionsFinished().then(() => {
|
await db.whenTransactionsFinished();
|
||||||
conn._setSyncedWith(sender);
|
conn._setSyncedWith(sender);
|
||||||
defer.resolve();
|
defer.resolve();
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function extendConnector (Y/* :any */) {
|
function extendConnector (Y/* :any */) {
|
||||||
@@ -667,12 +639,10 @@ function extendConnector (Y/* :any */) {
|
|||||||
this.setUserId(Y.utils.generateUserId());
|
this.setUserId(Y.utils.generateUserId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reconnect () {
|
reconnect () {
|
||||||
this.log('reconnecting..');
|
this.log('reconnecting..');
|
||||||
return this.y.db.startGarbageCollector()
|
return this.y.db.startGarbageCollector()
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnect () {
|
disconnect () {
|
||||||
this.log('discronnecting..');
|
this.log('discronnecting..');
|
||||||
this.connections = new Map();
|
this.connections = new Map();
|
||||||
@@ -682,16 +652,13 @@ function extendConnector (Y/* :any */) {
|
|||||||
this.y.db.stopGarbageCollector();
|
this.y.db.stopGarbageCollector();
|
||||||
return this.y.db.whenTransactionsFinished()
|
return this.y.db.whenTransactionsFinished()
|
||||||
}
|
}
|
||||||
|
|
||||||
repair () {
|
repair () {
|
||||||
this.log('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');
|
this.log('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');
|
||||||
|
this.connections.forEach(user => { user.isSynced = false; });
|
||||||
this.isSynced = false;
|
this.isSynced = false;
|
||||||
this.connections.forEach((user, userId) => {
|
this.currentSyncTarget = null;
|
||||||
user.isSynced = false;
|
this.findNextSyncTarget();
|
||||||
this._syncWithUser(userId);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setUserId (userId) {
|
setUserId (userId) {
|
||||||
if (this.userId == null) {
|
if (this.userId == null) {
|
||||||
if (!Number.isInteger(userId)) {
|
if (!Number.isInteger(userId)) {
|
||||||
@@ -706,21 +673,20 @@ function extendConnector (Y/* :any */) {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onUserEvent (f) {
|
onUserEvent (f) {
|
||||||
this.userEventListeners.push(f);
|
this.userEventListeners.push(f);
|
||||||
}
|
}
|
||||||
|
|
||||||
removeUserEventListener (f) {
|
removeUserEventListener (f) {
|
||||||
this.userEventListeners = this.userEventListeners.filter(g => f !== g);
|
this.userEventListeners = this.userEventListeners.filter(g => f !== g);
|
||||||
}
|
}
|
||||||
|
|
||||||
userLeft (user) {
|
userLeft (user) {
|
||||||
if (this.connections.has(user)) {
|
if (this.connections.has(user)) {
|
||||||
this.log('%s: User left %s', this.userId, user);
|
this.log('%s: User left %s', this.userId, user);
|
||||||
this.connections.delete(user);
|
this.connections.delete(user);
|
||||||
// check if isSynced event can be sent now
|
if (user === this.currentSyncTarget) {
|
||||||
this._setSyncedWith(null);
|
this.currentSyncTarget = null;
|
||||||
|
this.findNextSyncTarget();
|
||||||
|
}
|
||||||
for (var f of this.userEventListeners) {
|
for (var f of this.userEventListeners) {
|
||||||
f({
|
f({
|
||||||
action: 'userLeft',
|
action: 'userLeft',
|
||||||
@@ -729,7 +695,7 @@ function extendConnector (Y/* :any */) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
userJoined (user, role, auth) {
|
userJoined (user, role) {
|
||||||
if (role == null) {
|
if (role == null) {
|
||||||
throw new Error('You must specify the role of the joined user!')
|
throw new Error('You must specify the role of the joined user!')
|
||||||
}
|
}
|
||||||
@@ -742,7 +708,7 @@ function extendConnector (Y/* :any */) {
|
|||||||
isSynced: false,
|
isSynced: false,
|
||||||
role: role,
|
role: role,
|
||||||
processAfterAuth: [],
|
processAfterAuth: [],
|
||||||
auth: auth || null,
|
auth: null,
|
||||||
receivedSyncStep2: false
|
receivedSyncStep2: false
|
||||||
});
|
});
|
||||||
let defer = {};
|
let defer = {};
|
||||||
@@ -755,7 +721,9 @@ function extendConnector (Y/* :any */) {
|
|||||||
role: role
|
role: role
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
this._syncWithUser(user);
|
if (this.currentSyncTarget == null) {
|
||||||
|
this.findNextSyncTarget();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Execute a function _when_ we are connected.
|
// Execute a function _when_ we are connected.
|
||||||
// If not connected, wait until connected
|
// If not connected, wait until connected
|
||||||
@@ -766,25 +734,39 @@ function extendConnector (Y/* :any */) {
|
|||||||
this.whenSyncedListeners.push(f);
|
this.whenSyncedListeners.push(f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_syncWithUser (userid) {
|
findNextSyncTarget () {
|
||||||
if (this.role === 'slave') {
|
if (this.currentSyncTarget != null || this.role === 'slave') {
|
||||||
return // "The current sync has not finished or this is controlled by a master!"
|
return // "The current sync has not finished or this is controlled by a master!"
|
||||||
}
|
}
|
||||||
sendSyncStep1(this, userid);
|
|
||||||
}
|
var syncUser = null;
|
||||||
_fireIsSyncedListeners () {
|
for (var [uid, user] of this.connections) {
|
||||||
this.y.db.whenTransactionsFinished().then(() => {
|
if (!user.isSynced) {
|
||||||
if (!this.isSynced) {
|
syncUser = uid;
|
||||||
this.isSynced = true;
|
break
|
||||||
// It is safer to remove this!
|
|
||||||
// TODO: remove: yield * this.garbageCollectAfterSync()
|
|
||||||
// call whensynced listeners
|
|
||||||
for (var f of this.whenSyncedListeners) {
|
|
||||||
f();
|
|
||||||
}
|
|
||||||
this.whenSyncedListeners = [];
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
var conn = this;
|
||||||
|
if (syncUser != null) {
|
||||||
|
this.currentSyncTarget = syncUser;
|
||||||
|
sendSyncStep1(this, syncUser);
|
||||||
|
} else {
|
||||||
|
if (!conn.isSynced) {
|
||||||
|
this.y.db.requestTransaction(function * () {
|
||||||
|
if (!conn.isSynced) {
|
||||||
|
// it is crucial that isSynced is set at the time garbageCollectAfterSync is called
|
||||||
|
conn.isSynced = true;
|
||||||
|
// It is safer to remove this!
|
||||||
|
// TODO: remove: yield * this.garbageCollectAfterSync()
|
||||||
|
// call whensynced listeners
|
||||||
|
for (var f of conn.whenSyncedListeners) {
|
||||||
|
f();
|
||||||
|
}
|
||||||
|
conn.whenSyncedListeners = [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
send (uid, buffer) {
|
send (uid, buffer) {
|
||||||
if (!(buffer instanceof ArrayBuffer || buffer instanceof Uint8Array)) {
|
if (!(buffer instanceof ArrayBuffer || buffer instanceof Uint8Array)) {
|
||||||
@@ -834,13 +816,12 @@ function extendConnector (Y/* :any */) {
|
|||||||
/*
|
/*
|
||||||
You received a raw message, and you know that it is intended for Yjs. Then call this function.
|
You received a raw message, and you know that it is intended for Yjs. Then call this function.
|
||||||
*/
|
*/
|
||||||
receiveMessage (sender, buffer, skipAuth) {
|
async receiveMessage (sender, buffer) {
|
||||||
skipAuth = skipAuth || false;
|
|
||||||
if (!(buffer instanceof ArrayBuffer || buffer instanceof Uint8Array)) {
|
if (!(buffer instanceof ArrayBuffer || buffer instanceof Uint8Array)) {
|
||||||
return Promise.reject(new Error('Expected Message to be an ArrayBuffer or Uint8Array!'))
|
throw new Error('Expected Message to be an ArrayBuffer or Uint8Array!')
|
||||||
}
|
}
|
||||||
if (sender === this.userId) {
|
if (sender === this.userId) {
|
||||||
return Promise.resolve()
|
return
|
||||||
}
|
}
|
||||||
let decoder = new BinaryDecoder(buffer);
|
let decoder = new BinaryDecoder(buffer);
|
||||||
let encoder = new BinaryEncoder();
|
let encoder = new BinaryEncoder();
|
||||||
@@ -852,63 +833,60 @@ function extendConnector (Y/* :any */) {
|
|||||||
this.log('%s: Receive \'%s\' from %s', this.userId, messageType, sender);
|
this.log('%s: Receive \'%s\' from %s', this.userId, messageType, sender);
|
||||||
this.logMessage('Message: %Y', buffer);
|
this.logMessage('Message: %Y', buffer);
|
||||||
|
|
||||||
if (senderConn == null && !skipAuth) {
|
if (senderConn == null) {
|
||||||
throw new Error('Received message from unknown peer!')
|
throw new Error('Received message from unknown peer!')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (messageType === 'sync step 1' || messageType === 'sync step 2') {
|
if (messageType === 'sync step 1' || messageType === 'sync step 2') {
|
||||||
let auth = decoder.readVarUint();
|
let auth = decoder.readVarUint();
|
||||||
if (senderConn.auth == null) {
|
if (senderConn.auth == null) {
|
||||||
senderConn.processAfterAuth.push([messageType, senderConn, decoder, encoder, sender]);
|
|
||||||
// check auth
|
// check auth
|
||||||
return this.checkAuth(auth, this.y, sender).then(authPermissions => {
|
let authPermissions = await this.checkAuth(auth, this.y, sender);
|
||||||
if (senderConn.auth == null) {
|
senderConn.auth = authPermissions;
|
||||||
senderConn.auth = authPermissions;
|
this.y.emit('userAuthenticated', {
|
||||||
this.y.emit('userAuthenticated', {
|
user: senderConn.uid,
|
||||||
user: senderConn.uid,
|
auth: authPermissions
|
||||||
auth: authPermissions
|
});
|
||||||
});
|
senderConn.syncStep2.promise.then(() => {
|
||||||
|
if (senderConn.processAfterAuth == null) {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
let messages = senderConn.processAfterAuth;
|
for (let i = 0; i < senderConn.processAfterAuth.length; i++) {
|
||||||
senderConn.processAfterAuth = [];
|
let m = senderConn.processAfterAuth[i];
|
||||||
|
this.receiveMessage(m[0], m[1]);
|
||||||
return messages.reduce((p, m) =>
|
}
|
||||||
p.then(() => this.computeMessage(m[0], m[1], m[2], m[3], m[4]))
|
senderConn.processAfterAuth = null;
|
||||||
, Promise.resolve())
|
});
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (skipAuth || senderConn.auth != null) {
|
|
||||||
return this.computeMessage(messageType, senderConn, decoder, encoder, sender, skipAuth)
|
|
||||||
} else {
|
|
||||||
senderConn.processAfterAuth.push([messageType, senderConn, decoder, encoder, sender, false]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
computeMessage (messageType, senderConn, decoder, encoder, sender, skipAuth) {
|
if (senderConn.auth == null) {
|
||||||
|
senderConn.processAfterAuth.push([sender, buffer]);
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (messageType === 'sync step 1' && (senderConn.auth === 'write' || senderConn.auth === 'read')) {
|
if (messageType === 'sync step 1' && (senderConn.auth === 'write' || senderConn.auth === 'read')) {
|
||||||
// cannot wait for sync step 1 to finish, because we may wait for sync step 2 in sync step 1 (->lock)
|
// cannot wait for sync step 1 to finish, because we may wait for sync step 2 in sync step 1 (->lock)
|
||||||
computeMessageSyncStep1(decoder, encoder, this, senderConn, sender);
|
computeMessageSyncStep1(decoder, encoder, this, senderConn, sender);
|
||||||
return this.y.db.whenTransactionsFinished()
|
return this.y.db.whenTransactionsFinished()
|
||||||
} else if (messageType === 'sync step 2' && senderConn.auth === 'write') {
|
} else if (messageType === 'sync step 2' && senderConn.auth === 'write') {
|
||||||
return computeMessageSyncStep2(decoder, encoder, this, senderConn, sender)
|
return computeMessageSyncStep2(decoder, encoder, this, senderConn, sender)
|
||||||
} else if (messageType === 'update' && (skipAuth || senderConn.auth === 'write')) {
|
} else if (messageType === 'update' && senderConn.auth === 'write') {
|
||||||
return computeMessageUpdate(decoder, encoder, this, senderConn, sender)
|
return computeMessageUpdate(decoder, encoder, this, senderConn, sender)
|
||||||
} else {
|
} else {
|
||||||
return Promise.reject(new Error('Unable to receive message'))
|
console.error('Unable to receive message');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_setSyncedWith (user) {
|
_setSyncedWith (user) {
|
||||||
if (user != null) {
|
var conn = this.connections.get(user);
|
||||||
this.connections.get(user).isSynced = true;
|
if (conn != null) {
|
||||||
|
conn.isSynced = true;
|
||||||
}
|
}
|
||||||
let conns = Array.from(this.connections.values());
|
if (user === this.currentSyncTarget) {
|
||||||
if (conns.length > 0 && conns.every(u => u.isSynced)) {
|
this.currentSyncTarget = null;
|
||||||
this._fireIsSyncedListeners();
|
this.findNextSyncTarget();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Currently, the HB encodes operations as JSON. For the moment I want to keep it
|
Currently, the HB encodes operations as JSON. For the moment I want to keep it
|
||||||
that way. Maybe we support encoding in the HB as XML in the future, but for now I don't want
|
that way. Maybe we support encoding in the HB as XML in the future, but for now I don't want
|
||||||
@@ -1003,48 +981,6 @@ function extendConnector (Y/* :any */) {
|
|||||||
Y.AbstractConnector = AbstractConnector;
|
Y.AbstractConnector = AbstractConnector;
|
||||||
}
|
}
|
||||||
|
|
||||||
function extendPersistence (Y) {
|
|
||||||
class AbstractPersistence {
|
|
||||||
constructor (y, opts) {
|
|
||||||
this.y = y;
|
|
||||||
this.opts = opts;
|
|
||||||
this.saveOperationsBuffer = [];
|
|
||||||
this.log = Y.debug('y:persistence');
|
|
||||||
}
|
|
||||||
saveToMessageQueue (binary) {
|
|
||||||
this.log('Room %s: Save message to message queue', this.y.options.connector.room);
|
|
||||||
}
|
|
||||||
saveOperations (ops) {
|
|
||||||
ops = ops.map(function (op) {
|
|
||||||
return Y.Struct[op.struct].encode(op)
|
|
||||||
});
|
|
||||||
const saveOperations = () => {
|
|
||||||
if (this.saveOperationsBuffer.length > 0) {
|
|
||||||
let encoder = new BinaryEncoder();
|
|
||||||
encoder.writeVarString(this.opts.room);
|
|
||||||
encoder.writeVarString('update');
|
|
||||||
let ops = this.saveOperationsBuffer;
|
|
||||||
this.saveOperationsBuffer = [];
|
|
||||||
let length = ops.length;
|
|
||||||
encoder.writeUint32(length);
|
|
||||||
for (var i = 0; i < length; i++) {
|
|
||||||
let op = ops[i];
|
|
||||||
Y.Struct[op.struct].binaryEncode(encoder, op);
|
|
||||||
}
|
|
||||||
this.saveToMessageQueue(encoder.createBuffer());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if (this.saveOperationsBuffer.length === 0) {
|
|
||||||
this.saveOperationsBuffer = ops;
|
|
||||||
this.y.db.whenTransactionsFinished().then(saveOperations);
|
|
||||||
} else {
|
|
||||||
this.saveOperationsBuffer = this.saveOperationsBuffer.concat(ops);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Y.AbstractPersistence = AbstractPersistence;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* @flow */
|
/* @flow */
|
||||||
function extendDatabase (Y /* :any */) {
|
function extendDatabase (Y /* :any */) {
|
||||||
/*
|
/*
|
||||||
@@ -1165,7 +1101,7 @@ function extendDatabase (Y /* :any */) {
|
|||||||
startGarbageCollector () {
|
startGarbageCollector () {
|
||||||
this.gc = this.dbOpts.gc;
|
this.gc = this.dbOpts.gc;
|
||||||
if (this.gc) {
|
if (this.gc) {
|
||||||
this.gcTimeout = !this.dbOpts.gcTimeout ? 30000 : this.dbOpts.gcTimeout;
|
this.gcTimeout = !this.dbOpts.gcTimeout ? 100000 : this.dbOpts.gcTimeout;
|
||||||
} else {
|
} else {
|
||||||
this.gcTimeout = -1;
|
this.gcTimeout = -1;
|
||||||
}
|
}
|
||||||
@@ -1749,9 +1685,6 @@ function extendTransaction (Y) {
|
|||||||
if (send.length > 0) { // TODO: && !this.store.forwardAppliedOperations (but then i don't send delete ops)
|
if (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);
|
||||||
if (this.store.y.persistence != null) {
|
|
||||||
this.store.y.persistence.saveOperations(send);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2334,15 +2267,10 @@ function extendTransaction (Y) {
|
|||||||
yield * this.garbageCollectOperation(o.id);
|
yield * this.garbageCollectOperation(o.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.store.forwardAppliedOperations || this.store.y.persistence != null) {
|
if (this.store.forwardAppliedOperations) {
|
||||||
var ops = [];
|
var ops = [];
|
||||||
ops.push({struct: 'Delete', target: [del[0], del[1]], length: del[2]});
|
ops.push({struct: 'Delete', target: [del[0], del[1]], length: del[2]});
|
||||||
if (this.store.forwardAppliedOperations) {
|
this.store.y.connector.broadcastOps(ops);
|
||||||
this.store.y.connector.broadcastOps(ops);
|
|
||||||
}
|
|
||||||
if (this.store.y.persistence != null) {
|
|
||||||
this.store.y.persistence.saveOperations(ops);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2393,14 +2321,9 @@ function extendTransaction (Y) {
|
|||||||
* addOperation (op) {
|
* addOperation (op) {
|
||||||
yield * this.os.put(op);
|
yield * this.os.put(op);
|
||||||
// case op is created by this user, op is already broadcasted in applyCreatedOperations
|
// case op is created by this user, op is already broadcasted in applyCreatedOperations
|
||||||
if (op.id[0] !== this.store.userId && typeof op.id[1] !== 'string') {
|
if (op.id[0] !== this.store.userId && this.store.forwardAppliedOperations && typeof op.id[1] !== 'string') {
|
||||||
if (this.store.forwardAppliedOperations) {
|
// 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]);
|
|
||||||
}
|
|
||||||
if (this.store.y.persistence != null) {
|
|
||||||
this.store.y.persistence.saveOperations([op]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// if insertion, try to combine with left insertion (if both have content property)
|
// if insertion, try to combine with left insertion (if both have content property)
|
||||||
@@ -2737,20 +2660,6 @@ function extendTransaction (Y) {
|
|||||||
Y.Struct[op.struct].binaryEncode(encoder, Y.Struct[op.struct].encode(op));
|
Y.Struct[op.struct].binaryEncode(encoder, Y.Struct[op.struct].encode(op));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
* toBinary () {
|
|
||||||
let encoder = new BinaryEncoder();
|
|
||||||
yield * this.writeOperationsUntransformed(encoder);
|
|
||||||
yield * this.writeDeleteSet(encoder);
|
|
||||||
return encoder.createBuffer()
|
|
||||||
}
|
|
||||||
|
|
||||||
* fromBinary (buffer) {
|
|
||||||
let decoder = new BinaryDecoder(buffer);
|
|
||||||
yield * this.applyOperationsUntransformed(decoder);
|
|
||||||
yield * this.applyDeleteSet(decoder);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Get the plain untransformed operations from the database.
|
* Get the plain untransformed operations from the database.
|
||||||
* You can apply these operations using .applyOperationsUntransformed(ops)
|
* You can apply these operations using .applyOperationsUntransformed(ops)
|
||||||
@@ -4798,7 +4707,6 @@ function localstorage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
extendConnector(Y);
|
extendConnector(Y);
|
||||||
extendPersistence(Y);
|
|
||||||
extendDatabase(Y);
|
extendDatabase(Y);
|
||||||
extendTransaction(Y);
|
extendTransaction(Y);
|
||||||
extendStruct(Y);
|
extendStruct(Y);
|
||||||
@@ -4951,11 +4859,6 @@ class YConfig extends Y.utils.NamedEventHandler {
|
|||||||
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);
|
||||||
if (opts.persistence != null) {
|
|
||||||
this.persistence = new Y[opts.persistence.name](this, opts.persistence);
|
|
||||||
} else {
|
|
||||||
this.persistence = null;
|
|
||||||
}
|
|
||||||
this.connected = true;
|
this.connected = true;
|
||||||
}
|
}
|
||||||
init (callback) {
|
init (callback) {
|
||||||
@@ -4985,15 +4888,9 @@ class YConfig extends Y.utils.NamedEventHandler {
|
|||||||
}
|
}
|
||||||
share[propertyname] = yield * this.store.initType.call(this, id, args);
|
share[propertyname] = yield * this.store.initType.call(this, id, args);
|
||||||
}
|
}
|
||||||
|
this.store.whenTransactionsFinished()
|
||||||
|
.then(callback);
|
||||||
});
|
});
|
||||||
if (this.persistence != null) {
|
|
||||||
this.persistence.retrieveContent()
|
|
||||||
.then(() => this.db.whenTransactionsFinished())
|
|
||||||
.then(callback);
|
|
||||||
} else {
|
|
||||||
this.db.whenTransactionsFinished()
|
|
||||||
.then(callback);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
isConnected () {
|
isConnected () {
|
||||||
return this.connector.isSynced
|
return this.connector.isSynced
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user