Compare commits

...

29 Commits

Author SHA1 Message Date
Kevin Jahns
fd24c85437 v13.0.0-19 -- distribution files 2017-09-26 21:53:13 +02:00
Kevin Jahns
be537c9f8c 13.0.0-19 2017-09-26 21:53:01 +02:00
Kevin Jahns
4028eee39d implemented chunked broadcast of updates 2017-09-26 21:52:07 +02:00
Kevin Jahns
0e3e561ec7 13.0.0-18 2017-09-20 11:34:03 +02:00
Kevin Jahns
7df46cb731 Merge branch 'master' of github.com:y-js/yjs 2017-09-20 11:30:24 +02:00
Kevin Jahns
40fb16ef32 catch y-* related errors 2017-09-20 11:29:13 +02:00
Kevin Jahns
ada5d36cd5 add more y-xml tests 2017-09-19 03:16:48 +02:00
Kevin Jahns
f537a43e29 implement tests for dom filter 2017-09-18 22:14:45 +02:00
Kevin Jahns
3a305fb228 13.0.0-17 2017-09-11 17:38:21 +02:00
Kevin Jahns
1afdab376d fix linting 2017-09-11 17:37:39 +02:00
Kevin Jahns
526c862071 added test case for moving nodes 2017-09-11 17:35:20 +02:00
Kevin Jahns
fdbb558ce2 persistence db fixes 2017-09-11 16:02:19 +02:00
Kevin Jahns
76ad58bb59 fix example dist script 2017-09-07 23:02:19 +02:00
Kevin Jahns
c88a813bb0 fix tests by removing y-memory include 2017-09-06 20:52:52 +02:00
Kevin Jahns
ccf6d86c98 removed generators 2017-09-06 20:10:38 +02:00
Kevin Jahns
6b5c02f1ce 13.0.0-16 2017-08-26 01:11:31 +02:00
Kevin Jahns
2be6e935a4 fix lint in xml tests 2017-08-26 01:10:50 +02:00
Kevin Jahns
0ddf3bf742 Y.Xml renamed to Y.XmlElement 2017-08-25 20:35:17 +02:00
Kevin Jahns
5f29724578 merge textarea example 2017-08-24 14:46:16 +02:00
Kevin Jahns
ab6cde07e6 Implemented Xml Struct 2017-08-24 14:44:40 +02:00
Kevin Jahns
0455eaa8ad 13.0.0-15 2017-08-14 15:53:54 +02:00
Kevin Jahns
9ed7e15d0f 13.0.0-14 2017-08-14 15:49:15 +02:00
Kevin Jahns
6e633d0bd9 lint 2017-08-14 15:41:37 +02:00
Kevin Jahns
e16195cb54 implement timeout for creating Yjs instance 2017-08-14 15:39:17 +02:00
Kevin Jahns
86c46cf0ec 13.0.0-13 2017-08-13 01:04:37 +02:00
Kevin Jahns
8770c8e934 Implement persistence layer 2017-08-13 01:03:54 +02:00
Kevin Jahns
7e12ea2db5 move array tests and map tests to yjs 2017-08-09 02:21:17 +02:00
Kevin Jahns
3ca260e0da 13.0.0-12 2017-08-04 18:07:44 +02:00
Kevin Jahns
edb5e4f719 send sync step 1 after sync step 2 is processed (for slaves) 2017-08-04 18:06:36 +02:00
40 changed files with 34068 additions and 2272 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
node_modules
bower_components
/y.*
/examples/yjs-dist.js*

View File

@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
</head>
<!-- jquery is not required for y-xml. It is just here for convenience, and to test batch operations. -->
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<script src="../yjs-dist.js"></script>
<script src="./index.js"></script>
</head>
<body contenteditable="true">
</body>
</html>

View File

@@ -0,0 +1,21 @@
/* global Y */
// initialize a shared object. This function call returns a promise!
Y({
db: {
name: 'memory'
},
connector: {
name: 'websockets-client',
// url: 'http://127.0.0.1:1234',
url: 'http://192.168.178.81:1234',
room: 'html-editor-example6'
},
share: {
xml: 'XmlFragment()' // y.share.xml is of type Y.Xml with tagname "p"
}
}).then(function (y) {
window.yXml = y
// Bind children of XmlFragment to the document.body
window.yXml.share.xml.bindToDom(document.body)
})

View File

@@ -0,0 +1,58 @@
<!DOCTYPE html>
<html>
<body>
<style>
.wrapper {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: 7px;
}
.one {
grid-column: 1 ;
}
.two {
grid-column: 2;
}
.three {
grid-column: 3;
}
textarea {
width: calc(100% - 10px)
}
.editor-container {
background-color: #4caf50;
padding: 4px 5px 10px 5px;
border-radius: 11px;
}
.editor-container[disconnected] {
background-color: red;
}
.disconnected-info {
display: none;
}
.editor-container[disconnected] .disconnected-info {
display: inline;
}
</style>
<div class="wrapper">
<div id="container1" class="one editor-container">
<h1>Server 1 <span class="disconnected-info">(disconnected)</span></h1>
<textarea id="textarea1" rows=40 autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
</div>
<div id="container2" class="two editor-container">
<h1>Server 2 <span class="disconnected-info">(disconnected)</span></h1>
<textarea id="textarea2" rows=40 autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
</div>
<div id="container3" class="three editor-container">
<h1>Server 3 <span class="disconnected-info">(disconnected)</span></h1>
<textarea id="textarea3" rows=40 autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
</div>
</div>
<script src="../../y.js"></script>
<script src="../../../y-array/y-array.js"></script>
<script src="../../../y-text/dist/y-text.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>
</body>
</html>

View File

@@ -0,0 +1,64 @@
/* global Y */
Y({
db: {
name: 'memory'
},
connector: {
name: 'websockets-client',
room: 'Textarea-example',
url: 'https://yjs-v13.herokuapp.com/'
},
share: {
textarea: 'Text'
}
}).then(function (y) {
window.y1 = y
y.share.textarea.bind(document.getElementById('textarea1'))
})
Y({
db: {
name: 'memory'
},
connector: {
name: 'websockets-client',
room: 'Textarea-example',
url: 'https://yjs-v13-second.herokuapp.com/'
},
share: {
textarea: 'Text'
}
}).then(function (y) {
window.y2 = y
y.share.textarea.bind(document.getElementById('textarea2'))
y.connector.socket.on('connection', function () {
document.getElementById('container2').removeAttribute('disconnected')
})
y.connector.socket.on('disconnect', function () {
document.getElementById('container2').setAttribute('disconnected', true)
})
})
Y({
db: {
name: 'memory'
},
connector: {
name: 'websockets-client',
room: 'Textarea-example',
url: 'https://yjs-v13-third.herokuapp.com/'
},
share: {
textarea: 'Text'
}
}).then(function (y) {
window.y3 = y
y.share.textarea.bind(document.getElementById('textarea3'))
y.connector.socket.on('connection', function () {
document.getElementById('container3').removeAttribute('disconnected')
})
y.connector.socket.on('disconnect', function () {
document.getElementById('container3').setAttribute('disconnected', true)
})
})

View File

@@ -2,6 +2,10 @@
"name": "examples",
"version": "0.0.0",
"description": "",
"scripts": {
"dist": "rollup -c",
"watch": "rollup -cw"
},
"author": "Kevin Jahns",
"license": "MIT",
"dependencies": {

27
examples/rollup.config.js Normal file
View File

@@ -0,0 +1,27 @@
import nodeResolve from 'rollup-plugin-node-resolve'
import commonjs from 'rollup-plugin-commonjs'
var pkg = require('./package.json')
export default {
entry: 'yjs-dist.esm',
dest: 'yjs-dist.js',
moduleName: 'Y',
format: 'umd',
plugins: [
nodeResolve({
main: true,
module: true,
browser: true
}),
commonjs()
],
sourceMap: true,
banner: `
/**
* ${pkg.name} - ${pkg.description}
* @version v${pkg.version}
* @license ${pkg.license}
*/
`
}

View File

@@ -4,8 +4,7 @@
<textarea style="width:80%;" rows=40 id="textfield" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
<script src="../../y.js"></script>
<script src="../../../y-array/y-array.js"></script>
<script src="../../../y-text/dist/y-text.js"></script>
<script src="../../../y-memory/y-memory.js"></script>
<script src="../../../y-text/y-text.js"></script>
<script src="../../../y-websockets-client/y-websockets-client.js"></script>
<script src="./index.js"></script>
</body>

View File

@@ -7,18 +7,17 @@ Y({
},
connector: {
name: 'websockets-client',
room: 'Textarea-example',
url: 'http://127.0.0.1:1234'
room: 'Textarea-example2',
// url: '//localhost:1234',
url: 'https://yjs-v13.herokuapp.com/'
},
sourceDir: '/bower_components',
share: {
textarea: 'Text', // y.share.textarea is of type Y.Text
test: 'Array'
}
textarea: 'Text'
},
timeout: 5000 // reject if no connection was established within 5 seconds
}).then(function (y) {
window.yTextarea = y
// bind the textarea to a shared text element
y.share.textarea.bind(document.getElementById('textfield'))
// thats it..
})

View File

@@ -1,8 +1,9 @@
<!DOCTYPE html>
<html>
</head>
<script src="../bower_components/yjs/y.js"></script>
<script src="../bower_components/jquery/dist/jquery.min.js"></script>
<!-- jquery is not required for y-xml. It is just here for convenience, and to test batch operations. -->
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<script src="../yjs-dist.js"></script>
<script src="./index.js"></script>
</head>
<body>

View File

@@ -7,6 +7,8 @@ Y({
},
connector: {
name: 'websockets-client',
// url: 'http://127.0.0.1:1234',
url: 'http://192.168.178.81:1234',
room: 'Xml-example'
},
sourceDir: '/bower_components',

12
examples/yjs-dist.esm Normal file
View File

@@ -0,0 +1,12 @@
import Y from '../src/y.js'
import yArray from '../../y-array/src/y-array.js'
import yIndexedDB from '../../y-indexeddb/src/y-indexeddb.js'
import yMap from '../../y-map/src/y-map.js'
import yText from '../../y-text/src/Text.js'
import yXml from '../../y-xml/src/y-xml.js'
import yWebsocketsClient from '../../y-websockets-client/src/y-websockets-client.js'
Y.extend(yArray, yIndexedDB, yMap, yText, yXml, yWebsocketsClient)
export default Y

2499
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "yjs",
"version": "13.0.0-11",
"version": "13.0.0-19",
"description": "A framework for real-time p2p shared editing on any data",
"main": "./y.node.js",
"browser": "./y.js",
@@ -10,6 +10,7 @@
"debug": "concurrently 'rollup -wc rollup.test.js' 'cutest-serve y.test.js -o'",
"lint": "standard",
"dist": "rollup -c rollup.browser.js; rollup -c rollup.node.js",
"watch": "concurrently 'rollup -wc rollup.browser.js' 'rollup -wc rollup.node.js'",
"postversion": "npm run dist",
"postpublish": "tag-dist-files --overwrite-existing-tag"
},

View File

@@ -1,4 +1,3 @@
import inject from 'rollup-plugin-inject'
import babel from 'rollup-plugin-babel'
import uglify from 'rollup-plugin-uglify'
import nodeResolve from 'rollup-plugin-node-resolve'
@@ -16,12 +15,7 @@ export default {
browser: true
}),
commonjs(),
babel({
runtimeHelpers: true
}),
inject({
regeneratorRuntime: 'regenerator-runtime'
}),
babel(),
uglify({
output: {
comments: function (node, comment) {

View File

@@ -3,7 +3,7 @@ import commonjs from 'rollup-plugin-commonjs'
import multiEntry from 'rollup-plugin-multi-entry'
export default {
entry: 'test/*',
entry: 'test/*.js',
moduleName: 'y-tests',
format: 'umd',
plugins: [

View File

@@ -42,6 +42,11 @@ export default function extendConnector (Y/* :any */) {
if (opts.generateUserId !== false) {
this.setUserId(Y.utils.generateUserId())
}
if (opts.maxBufferLength == null) {
this.maxBufferLength = -1
} else {
this.maxBufferLength = opts.maxBufferLength
}
}
reconnect () {
@@ -105,6 +110,7 @@ export default function extendConnector (Y/* :any */) {
}
}
}
userJoined (user, role, auth) {
if (role == null) {
throw new Error('You must specify the role of the joined user!')
@@ -133,6 +139,7 @@ export default function extendConnector (Y/* :any */) {
}
this._syncWithUser(user)
}
// Execute a function _when_ we are connected.
// If not connected, wait until connected
whenSynced (f) {
@@ -142,18 +149,20 @@ export default function extendConnector (Y/* :any */) {
this.whenSyncedListeners.push(f)
}
}
_syncWithUser (userid) {
if (this.role === 'slave') {
return // "The current sync has not finished or this is controlled by a master!"
}
sendSyncStep1(this, userid)
}
_fireIsSyncedListeners () {
this.y.db.whenTransactionsFinished().then(() => {
if (!this.isSynced) {
this.isSynced = true
// It is safer to remove this!
// TODO: remove: yield * this.garbageCollectAfterSync()
// TODO: remove: this.garbageCollectAfterSync()
// call whensynced listeners
for (var f of this.whenSyncedListeners) {
f()
@@ -162,6 +171,7 @@ export default function extendConnector (Y/* :any */) {
}
})
}
send (uid, buffer) {
if (!(buffer instanceof ArrayBuffer || buffer instanceof Uint8Array)) {
throw new Error('Expected Message to be an ArrayBuffer or Uint8Array - please don\'t use this method to send custom messages')
@@ -169,6 +179,7 @@ export default function extendConnector (Y/* :any */) {
this.log('%s: Send \'%y\' to %s', this.userId, buffer, uid)
this.logMessage('Message: %Y', buffer)
}
broadcast (buffer) {
if (!(buffer instanceof ArrayBuffer || buffer instanceof Uint8Array)) {
throw new Error('Expected Message to be an ArrayBuffer or Uint8Array - please don\'t use this method to send custom messages')
@@ -176,6 +187,7 @@ export default function extendConnector (Y/* :any */) {
this.log('%s: Broadcast \'%y\'', this.userId, buffer)
this.logMessage('Message: %Y', buffer)
}
/*
Buffer operations, and broadcast them when ready.
*/
@@ -190,14 +202,19 @@ export default function extendConnector (Y/* :any */) {
encoder.writeVarString(self.opts.room)
encoder.writeVarString('update')
let ops = self.broadcastOpBuffer
self.broadcastOpBuffer = []
let length = ops.length
encoder.writeUint32(length)
for (var i = 0; i < length; i++) {
let encoderPosLen = encoder.pos
encoder.writeUint32(0)
for (var i = 0; i < length && (self.maxBufferLength < 0 || encoder.length < self.maxBufferLength); i++) {
let op = ops[i]
Y.Struct[op.struct].binaryEncode(encoder, op)
}
encoder.setUint32(encoderPosLen, i)
self.broadcastOpBuffer = ops.slice(i)
self.broadcast(encoder.createBuffer())
if (i !== length) {
setTimeout(broadcastOperations, 100)
}
}
}
if (this.broadcastOpBuffer.length === 0) {
@@ -207,10 +224,12 @@ export default function extendConnector (Y/* :any */) {
this.broadcastOpBuffer = this.broadcastOpBuffer.concat(ops)
}
}
/*
You received a raw message, and you know that it is intended for Yjs. Then call this function.
*/
receiveMessage (sender, buffer) {
receiveMessage (sender, buffer, skipAuth) {
skipAuth = skipAuth || false
if (!(buffer instanceof ArrayBuffer || buffer instanceof Uint8Array)) {
return Promise.reject(new Error('Expected Message to be an ArrayBuffer or Uint8Array!'))
}
@@ -223,14 +242,11 @@ export default function extendConnector (Y/* :any */) {
encoder.writeVarString(roomname)
let messageType = decoder.readVarString()
let senderConn = this.connections.get(sender)
this.log('%s: Receive \'%s\' from %s', this.userId, messageType, sender)
this.logMessage('Message: %Y', buffer)
if (senderConn == null) {
if (senderConn == null && !skipAuth) {
throw new Error('Received message from unknown peer!')
}
if (messageType === 'sync step 1' || messageType === 'sync step 2') {
let auth = decoder.readVarUint()
if (senderConn.auth == null) {
@@ -253,21 +269,21 @@ export default function extendConnector (Y/* :any */) {
})
}
}
if (senderConn.auth != null) {
return this.computeMessage(messageType, senderConn, decoder, encoder, sender)
if (skipAuth || senderConn.auth != null) {
return this.computeMessage(messageType, senderConn, decoder, encoder, sender, skipAuth)
} else {
senderConn.processAfterAuth.push([messageType, senderConn, decoder, encoder, sender])
senderConn.processAfterAuth.push([messageType, senderConn, decoder, encoder, sender, false])
}
}
computeMessage (messageType, senderConn, decoder, encoder, sender) {
computeMessage (messageType, senderConn, decoder, encoder, sender, skipAuth) {
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)
computeMessageSyncStep1(decoder, encoder, this, senderConn, sender)
return this.y.db.whenTransactionsFinished()
} else if (messageType === 'sync step 2' && senderConn.auth === 'write') {
return computeMessageSyncStep2(decoder, encoder, this, senderConn, sender)
} else if (messageType === 'update' && senderConn.auth === 'write') {
} else if (messageType === 'update' && (skipAuth || senderConn.auth === 'write')) {
return computeMessageUpdate(decoder, encoder, this, senderConn, sender)
} else {
return Promise.reject(new Error('Unable to receive message'))
@@ -283,97 +299,6 @@ export default function extendConnector (Y/* :any */) {
this._fireIsSyncedListeners()
}
}
/*
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
too much overhead. Y is very likely to get changed a lot in the future
Because we don't want to encode JSON as string (with character escaping, wich makes it pretty much unreadable)
we encode the JSON as XML.
When the HB support encoding as XML, the format should look pretty much like this.
does not support primitive values as array elements
expects an ltx (less than xml) object
*/
parseMessageFromXml (m/* :any */) {
function parseArray (node) {
for (var n of node.children) {
if (n.getAttribute('isArray') === 'true') {
return parseArray(n)
} else {
return parseObject(n)
}
}
}
function parseObject (node/* :any */) {
var json = {}
for (var attrName in node.attrs) {
var value = node.attrs[attrName]
var int = parseInt(value, 10)
if (isNaN(int) || ('' + int) !== value) {
json[attrName] = value
} else {
json[attrName] = int
}
}
for (var n/* :any */ in node.children) {
var name = n.name
if (n.getAttribute('isArray') === 'true') {
json[name] = parseArray(n)
} else {
json[name] = parseObject(n)
}
}
return json
}
parseObject(m)
}
/*
encode message in xml
we use string because Strophe only accepts an "xml-string"..
So {a:4,b:{c:5}} will look like
<y a="4">
<b c="5"></b>
</y>
m - ltx element
json - Object
*/
encodeMessageToXml (msg, obj) {
// attributes is optional
function encodeObject (m, json) {
for (var name in json) {
var value = json[name]
if (name == null) {
// nop
} else if (value.constructor === Object) {
encodeObject(m.c(name), value)
} else if (value.constructor === Array) {
encodeArray(m.c(name), value)
} else {
m.setAttribute(name, value)
}
}
}
function encodeArray (m, array) {
m.setAttribute('isArray', 'true')
for (var e of array) {
if (e.constructor === Object) {
encodeObject(m.c('array-element'), e)
} else {
encodeArray(m.c('array-element'), e)
}
}
}
if (obj.constructor === Object) {
encodeObject(msg.c('y', { xmlns: 'http://y.ninja/connector-stanza' }), obj)
} else if (obj.constructor === Array) {
encodeArray(msg.c('y', { xmlns: 'http://y.ninja/connector-stanza' }), obj)
} else {
throw new Error("I can't encode this json!")
}
}
}
Y.AbstractConnector = AbstractConnector
}

View File

@@ -85,11 +85,11 @@ export default function extendDatabase (Y /* :any */) {
console.warn('gc should be empty when not synced!')
}
return new Promise((resolve) => {
os.requestTransaction(function * () {
os.requestTransaction(function () {
if (os.y.connector != null && os.y.connector.isSynced) {
for (var i = 0; i < os.gc2.length; i++) {
var oid = os.gc2[i]
yield * this.garbageCollectOperation(oid)
this.garbageCollectOperation(oid)
}
os.gc2 = os.gc1
os.gc1 = []
@@ -120,7 +120,7 @@ export default function extendDatabase (Y /* :any */) {
startGarbageCollector () {
this.gc = this.dbOpts.gc
if (this.gc) {
this.gcTimeout = !this.dbOpts.gcTimeout ? 100000 : this.dbOpts.gcTimeout
this.gcTimeout = !this.dbOpts.gcTimeout ? 30000 : this.dbOpts.gcTimeout
} else {
this.gcTimeout = -1
}
@@ -197,15 +197,15 @@ export default function extendDatabase (Y /* :any */) {
this.gc = false
this.gcTimeout = -1
return new Promise(function (resolve) {
self.requestTransaction(function * () {
self.requestTransaction(function () {
var ungc /* :Array<Struct> */ = self.gc1.concat(self.gc2)
self.gc1 = []
self.gc2 = []
for (var i = 0; i < ungc.length; i++) {
var op = yield * this.getOperation(ungc[i])
var op = this.getOperation(ungc[i])
if (op != null) {
delete op.gc
yield * this.setOperation(op)
this.setOperation(op)
}
}
resolve()
@@ -224,7 +224,7 @@ export default function extendDatabase (Y /* :any */) {
returns true iff op was added to GC
*/
* addToGarbageCollector (op, left) {
addToGarbageCollector (op, left) {
if (
op.gc == null &&
op.deleted === true &&
@@ -235,12 +235,12 @@ export default function extendDatabase (Y /* :any */) {
if (left != null && left.deleted === true) {
gc = true
} else if (op.content != null && op.content.length > 1) {
op = yield * this.getInsertionCleanStart([op.id[0], op.id[1] + 1])
op = this.getInsertionCleanStart([op.id[0], op.id[1] + 1])
gc = true
}
if (gc) {
op.gc = true
yield * this.setOperation(op)
this.setOperation(op)
this.store.queueGarbageCollector(op.id)
return true
}
@@ -265,7 +265,7 @@ export default function extendDatabase (Y /* :any */) {
}
}
}
* destroy () {
destroy () {
clearTimeout(this.gcInterval)
this.gcInterval = null
this.stopRepairCheck()
@@ -274,9 +274,9 @@ export default function extendDatabase (Y /* :any */) {
if (!this.userIdPromise.inProgress) {
this.userIdPromise.inProgress = true
var self = this
self.requestTransaction(function * () {
self.requestTransaction(function () {
self.userId = userId
var state = yield * this.getState(userId)
var state = this.getState(userId)
self.opClock = state.clock
self.userIdPromise.resolve(userId)
})
@@ -355,7 +355,7 @@ export default function extendDatabase (Y /* :any */) {
this.listenersByIdRequestPending = true
var store = this
this.requestTransaction(function * () {
this.requestTransaction(function () {
var exeNow = store.listenersByIdExecuteNow
store.listenersByIdExecuteNow = []
@@ -366,7 +366,7 @@ export default function extendDatabase (Y /* :any */) {
for (let key = 0; key < exeNow.length; key++) {
let o = exeNow[key].op
yield * store.tryExecute.call(this, o)
store.tryExecute.call(this, o)
}
for (var sid in ls) {
@@ -374,9 +374,9 @@ export default function extendDatabase (Y /* :any */) {
var id = JSON.parse(sid)
var op
if (typeof id[1] === 'string') {
op = yield * this.getOperation(id)
op = this.getOperation(id)
} else {
op = yield * this.getInsertion(id)
op = this.getInsertion(id)
}
if (op == null) {
store.listenersById[sid] = l
@@ -385,7 +385,7 @@ export default function extendDatabase (Y /* :any */) {
let listener = l[i]
let o = listener.op
if (--listener.missing === 0) {
yield * store.tryExecute.call(this, o)
store.tryExecute.call(this, o)
}
}
}
@@ -402,15 +402,15 @@ export default function extendDatabase (Y /* :any */) {
addOperation: any;
whenOperationsExist: any;
*/
* tryExecute (op) {
this.store.addToDebug('yield * this.store.tryExecute.call(this, ', JSON.stringify(op), ')')
tryExecute (op) {
this.store.addToDebug('this.store.tryExecute.call(this, ', JSON.stringify(op), ')')
if (op.struct === 'Delete') {
yield * Y.Struct.Delete.execute.call(this, op)
Y.Struct.Delete.execute.call(this, op)
// this is now called in Transaction.deleteOperation!
// yield * this.store.operationAdded(this, op)
// this.store.operationAdded(this, op)
} else {
// check if this op was defined
var defined = yield * this.getInsertion(op.id)
var defined = this.getInsertion(op.id)
while (defined != null && defined.content != null) {
// check if this op has a longer content in the case it is defined
if (defined.id[1] + defined.content.length < op.id[1] + op.content.length) {
@@ -419,23 +419,23 @@ export default function extendDatabase (Y /* :any */) {
op.id = [op.id[0], op.id[1] + overlapSize]
op.left = Y.utils.getLastId(defined)
op.origin = op.left
defined = yield * this.getOperation(op.id) // getOperation suffices here
defined = this.getOperation(op.id) // getOperation suffices here
} else {
break
}
}
if (defined == null) {
var opid = op.id
var isGarbageCollected = yield * this.isGarbageCollected(opid)
var isGarbageCollected = this.isGarbageCollected(opid)
if (!isGarbageCollected) {
// TODO: reduce number of get / put calls for op ..
yield * Y.Struct[op.struct].execute.call(this, op)
yield * this.addOperation(op)
yield * this.store.operationAdded(this, op)
Y.Struct[op.struct].execute.call(this, op)
this.addOperation(op)
this.store.operationAdded(this, op)
// operationAdded can change op..
op = yield * this.getOperation(opid)
op = this.getOperation(opid)
// if insertion, try to combine with left
yield * this.tryCombineWithLeft(op)
this.tryCombineWithLeft(op)
}
}
}
@@ -452,15 +452,15 @@ export default function extendDatabase (Y /* :any */) {
* Always:
* * Call type
*/
* operationAdded (transaction, op) {
operationAdded (transaction, op) {
if (op.struct === 'Delete') {
var type = this.initializedTypes[JSON.stringify(op.targetParent)]
if (type != null) {
yield * type._changed(transaction, op)
type._changed(transaction, op)
}
} else {
// increase SS
yield * transaction.updateState(op.id[0])
transaction.updateState(op.id[0])
var opLen = op.content != null ? op.content.length : 1
for (let i = 0; i < opLen; i++) {
// notify whenOperation listeners (by id)
@@ -480,9 +480,9 @@ export default function extendDatabase (Y /* :any */) {
// if parent is deleted, mark as gc'd and return
if (op.parent != null) {
var parentIsDeleted = yield * transaction.isDeleted(op.parent)
var parentIsDeleted = transaction.isDeleted(op.parent)
if (parentIsDeleted) {
yield * transaction.deleteList(op.id)
transaction.deleteList(op.id)
return
}
}
@@ -490,7 +490,7 @@ export default function extendDatabase (Y /* :any */) {
// notify parent, if it was instanciated as a custom type
if (t != null) {
let o = Y.utils.copyOperation(op)
yield * t._changed(transaction, o)
t._changed(transaction, o)
}
if (!op.deleted) {
// Delete if DS says this is actually deleted
@@ -499,18 +499,19 @@ export default function extendDatabase (Y /* :any */) {
// TODO: !! console.log('TODO: change this before commiting')
for (let i = 0; i < len; i++) {
var id = [startId[0], startId[1] + i]
var opIsDeleted = yield * transaction.isDeleted(id)
var opIsDeleted = transaction.isDeleted(id)
if (opIsDeleted) {
var delop = {
struct: 'Delete',
target: id
}
yield * this.tryExecute.call(transaction, delop)
this.tryExecute.call(transaction, delop)
}
}
}
}
}
whenTransactionsFinished () {
if (this.transactionInProgress) {
if (this.transactionsFinished == null) {
@@ -528,6 +529,7 @@ export default function extendDatabase (Y /* :any */) {
return Promise.resolve()
}
}
// Check if there is another transaction request.
// * the last transaction is always a flush :)
getNextRequest () {
@@ -542,8 +544,8 @@ export default function extendDatabase (Y /* :any */) {
return null
} else {
this.transactionIsFlushed = true
return function * () {
yield * this.flush()
return function () {
this.flush()
}
}
} else {
@@ -570,13 +572,13 @@ export default function extendDatabase (Y /* :any */) {
Init type. This is called when a remote operation is retrieved, and transformed to a type
TODO: delete type from store.initializedTypes[id] when corresponding id was deleted!
*/
* initType (id, args) {
initType (id, args) {
var sid = JSON.stringify(id)
var t = this.store.initializedTypes[sid]
if (t == null) {
var op/* :MapStruct | ListStruct */ = yield * this.getOperation(id)
var op/* :MapStruct | ListStruct */ = this.getOperation(id)
if (op != null) {
t = yield * Y[op.type].typeDefinition.initType.call(this, this.store, op, args)
t = Y[op.type].typeDefinition.initType.call(this, this.store, op, args)
this.store.initializedTypes[sid] = t
}
}
@@ -588,14 +590,14 @@ export default function extendDatabase (Y /* :any */) {
createType (typedefinition, id) {
var structname = typedefinition[0].struct
id = id || this.getNextOpId(1)
var op = Y.Struct[structname].create(id)
var op = Y.Struct[structname].create(id, typedefinition[1])
op.type = typedefinition[0].name
this.requestTransaction(function * () {
this.requestTransaction(function () {
if (op.id[0] === 0xFFFFFF) {
yield * this.setOperation(op)
this.setOperation(op)
} else {
yield * this.applyCreatedOperations([op])
this.applyCreatedOperations([op])
}
})
var t = Y[op.type].typeDefinition.createType(this, op, typedefinition[1])

View File

@@ -1,354 +0,0 @@
/* global async, databases, describe, beforeEach, afterEach */
/* eslint-env browser,jasmine,console */
'use strict'
var Y = require('./SpecHelper.js')
for (let database of databases) {
describe(`Database (${database})`, function () {
var store
describe('DeleteStore', function () {
describe('Basic', function () {
beforeEach(function () {
store = new Y[database](null, {
gcTimeout: -1,
namespace: 'testing'
})
})
afterEach(function (done) {
store.requestTransaction(function * () {
yield * this.store.destroy()
done()
})
})
it('Deleted operation is deleted', async(function * (done) {
store.requestTransaction(function * () {
yield * this.markDeleted(['u1', 10], 1)
expect(yield * this.isDeleted(['u1', 10])).toBeTruthy()
expect(yield * this.getDeleteSet()).toEqual({'u1': [[10, 1, false]]})
done()
})
}))
it('Deleted operation extends other deleted operation', async(function * (done) {
store.requestTransaction(function * () {
yield * this.markDeleted(['u1', 10], 1)
yield * this.markDeleted(['u1', 11], 1)
expect(yield * this.isDeleted(['u1', 10])).toBeTruthy()
expect(yield * this.isDeleted(['u1', 11])).toBeTruthy()
expect(yield * this.getDeleteSet()).toEqual({'u1': [[10, 2, false]]})
done()
})
}))
it('Deleted operation extends other deleted operation', async(function * (done) {
store.requestTransaction(function * () {
yield * this.markDeleted(['0', 3], 1)
yield * this.markDeleted(['0', 4], 1)
yield * this.markDeleted(['0', 2], 1)
expect(yield * this.getDeleteSet()).toEqual({'0': [[2, 3, false]]})
done()
})
}))
it('Debug #1', async(function * (done) {
store.requestTransaction(function * () {
yield * this.markDeleted(['166', 0], 1)
yield * this.markDeleted(['166', 2], 1)
yield * this.markDeleted(['166', 0], 1)
yield * this.markDeleted(['166', 2], 1)
yield * this.markGarbageCollected(['166', 2], 1)
yield * this.markDeleted(['166', 1], 1)
yield * this.markDeleted(['166', 3], 1)
yield * this.markGarbageCollected(['166', 3], 1)
yield * this.markDeleted(['166', 0], 1)
expect(yield * this.getDeleteSet()).toEqual({'166': [[0, 2, false], [2, 2, true]]})
done()
})
}))
it('Debug #2', async(function * (done) {
store.requestTransaction(function * () {
yield * this.markDeleted(['293', 0], 1)
yield * this.markDeleted(['291', 2], 1)
yield * this.markDeleted(['291', 2], 1)
yield * this.markGarbageCollected(['293', 0], 1)
yield * this.markDeleted(['293', 1], 1)
yield * this.markGarbageCollected(['291', 2], 1)
expect(yield * this.getDeleteSet()).toEqual({'291': [[2, 1, true]], '293': [[0, 1, true], [1, 1, false]]})
done()
})
}))
it('Debug #3', async(function * (done) {
store.requestTransaction(function * () {
yield * this.markDeleted(['581', 0], 1)
yield * this.markDeleted(['581', 1], 1)
yield * this.markDeleted(['580', 0], 1)
yield * this.markDeleted(['580', 0], 1)
yield * this.markGarbageCollected(['581', 0], 1)
yield * this.markDeleted(['581', 2], 1)
yield * this.markDeleted(['580', 1], 1)
yield * this.markDeleted(['580', 2], 1)
yield * this.markDeleted(['580', 1], 1)
yield * this.markDeleted(['580', 2], 1)
yield * this.markGarbageCollected(['581', 2], 1)
yield * this.markGarbageCollected(['581', 1], 1)
yield * this.markGarbageCollected(['580', 1], 1)
expect(yield * this.getDeleteSet()).toEqual({'580': [[0, 1, false], [1, 1, true], [2, 1, false]], '581': [[0, 3, true]]})
done()
})
}))
it('Debug #4', async(function * (done) {
store.requestTransaction(function * () {
yield * this.markDeleted(['544', 0], 1)
yield * this.markDeleted(['543', 2], 1)
yield * this.markDeleted(['544', 0], 1)
yield * this.markDeleted(['543', 2], 1)
yield * this.markGarbageCollected(['544', 0], 1)
yield * this.markDeleted(['545', 1], 1)
yield * this.markDeleted(['543', 4], 1)
yield * this.markDeleted(['543', 3], 1)
yield * this.markDeleted(['544', 1], 1)
yield * this.markDeleted(['544', 2], 1)
yield * this.markDeleted(['544', 1], 1)
yield * this.markDeleted(['544', 2], 1)
yield * this.markGarbageCollected(['543', 2], 1)
yield * this.markGarbageCollected(['543', 4], 1)
yield * this.markGarbageCollected(['544', 2], 1)
yield * this.markGarbageCollected(['543', 3], 1)
expect(yield * this.getDeleteSet()).toEqual({'543': [[2, 3, true]], '544': [[0, 1, true], [1, 1, false], [2, 1, true]], '545': [[1, 1, false]]})
done()
})
}))
it('Debug #5', async(function * (done) {
store.requestTransaction(function * () {
yield * this.applyDeleteSet({'16': [[1, 2, false]], '17': [[0, 1, true], [1, 3, false]]})
expect(yield * this.getDeleteSet()).toEqual({'16': [[1, 2, false]], '17': [[0, 1, true], [1, 3, false]]})
yield * this.applyDeleteSet({'16': [[1, 2, false]], '17': [[0, 4, true]]})
expect(yield * this.getDeleteSet()).toEqual({'16': [[1, 2, false]], '17': [[0, 4, true]]})
done()
})
}))
it('Debug #6', async(function * (done) {
store.requestTransaction(function * () {
yield * this.applyDeleteSet({'40': [[0, 3, false]]})
expect(yield * this.getDeleteSet()).toEqual({'40': [[0, 3, false]]})
yield * this.applyDeleteSet({'39': [[2, 2, false]], '40': [[0, 1, true], [1, 2, false]], '41': [[2, 1, false]]})
expect(yield * this.getDeleteSet()).toEqual({'39': [[2, 2, false]], '40': [[0, 1, true], [1, 2, false]], '41': [[2, 1, false]]})
done()
})
}))
it('Debug #7', async(function * (done) {
store.requestTransaction(function * () {
yield * this.markDeleted(['9', 2], 1)
yield * this.markDeleted(['11', 2], 1)
yield * this.markDeleted(['11', 4], 1)
yield * this.markDeleted(['11', 1], 1)
yield * this.markDeleted(['9', 4], 1)
yield * this.markDeleted(['10', 0], 1)
yield * this.markGarbageCollected(['11', 2], 1)
yield * this.markDeleted(['11', 2], 1)
yield * this.markGarbageCollected(['11', 3], 1)
yield * this.markDeleted(['11', 3], 1)
yield * this.markDeleted(['11', 3], 1)
yield * this.markDeleted(['9', 4], 1)
yield * this.markDeleted(['10', 0], 1)
yield * this.markGarbageCollected(['11', 1], 1)
yield * this.markDeleted(['11', 1], 1)
expect(yield * this.getDeleteSet()).toEqual({'9': [[2, 1, false], [4, 1, false]], '10': [[0, 1, false]], '11': [[1, 3, true], [4, 1, false]]})
done()
})
}))
})
})
describe('OperationStore', function () {
describe('Basic Tests', function () {
beforeEach(function () {
store = new Y[database](null, {
gcTimeout: -1,
namespace: 'testing'
})
})
afterEach(function (done) {
store.requestTransaction(function * () {
yield * this.store.destroy()
done()
})
})
it('debug #1', function (done) {
store.requestTransaction(function * () {
yield * this.os.put({id: [2]})
yield * this.os.put({id: [0]})
yield * this.os.delete([2])
yield * this.os.put({id: [1]})
expect(yield * this.os.find([0])).toBeTruthy()
expect(yield * this.os.find([1])).toBeTruthy()
expect(yield * this.os.find([2])).toBeFalsy()
done()
})
})
it('can add&retrieve 5 elements', function (done) {
store.requestTransaction(function * () {
yield * this.os.put({val: 'four', id: [4]})
yield * this.os.put({val: 'one', id: [1]})
yield * this.os.put({val: 'three', id: [3]})
yield * this.os.put({val: 'two', id: [2]})
yield * this.os.put({val: 'five', id: [5]})
expect((yield * this.os.find([1])).val).toEqual('one')
expect((yield * this.os.find([2])).val).toEqual('two')
expect((yield * this.os.find([3])).val).toEqual('three')
expect((yield * this.os.find([4])).val).toEqual('four')
expect((yield * this.os.find([5])).val).toEqual('five')
done()
})
})
it('5 elements do not exist anymore after deleting them', function (done) {
store.requestTransaction(function * () {
yield * this.os.put({val: 'four', id: [4]})
yield * this.os.put({val: 'one', id: [1]})
yield * this.os.put({val: 'three', id: [3]})
yield * this.os.put({val: 'two', id: [2]})
yield * this.os.put({val: 'five', id: [5]})
yield * this.os.delete([4])
expect(yield * this.os.find([4])).not.toBeTruthy()
yield * this.os.delete([3])
expect(yield * this.os.find([3])).not.toBeTruthy()
yield * this.os.delete([2])
expect(yield * this.os.find([2])).not.toBeTruthy()
yield * this.os.delete([1])
expect(yield * this.os.find([1])).not.toBeTruthy()
yield * this.os.delete([5])
expect(yield * this.os.find([5])).not.toBeTruthy()
done()
})
})
})
var numberOfOSTests = 1000
describe(`Random Tests - after adding&deleting (0.8/0.2) ${numberOfOSTests} times`, function () {
var elements = []
beforeAll(function (done) {
store = new Y[database](null, {
gcTimeout: -1,
namespace: 'testing'
})
store.requestTransaction(function * () {
for (var i = 0; i < numberOfOSTests; i++) {
var r = Math.random()
if (r < 0.8) {
var obj = [Math.floor(Math.random() * numberOfOSTests * 10000)]
if (!(yield * this.os.find(obj))) {
elements.push(obj)
yield * this.os.put({id: obj})
}
} else if (elements.length > 0) {
var elemid = Math.floor(Math.random() * elements.length)
var elem = elements[elemid]
elements = elements.filter(function (e) {
return !Y.utils.compareIds(e, elem)
})
yield * this.os.delete(elem)
}
}
done()
})
})
afterAll(function (done) {
store.requestTransaction(function * () {
yield * this.store.destroy()
done()
})
})
it('can find every object', function (done) {
store.requestTransaction(function * () {
for (var id of elements) {
expect((yield * this.os.find(id)).id).toEqual(id)
}
done()
})
})
it('can find every object with lower bound search', function (done) {
store.requestTransaction(function * () {
for (var id of elements) {
var e = yield * this.os.findWithLowerBound(id)
expect(e.id).toEqual(id)
}
done()
})
})
it('iterating over a tree with lower bound yields the right amount of results', function (done) {
var lowerBound = elements[Math.floor(Math.random() * elements.length)]
var expectedResults = elements.filter(function (e, pos) {
return (Y.utils.smaller(lowerBound, e) || Y.utils.compareIds(e, lowerBound)) && elements.indexOf(e) === pos
}).length
var actualResults = 0
store.requestTransaction(function * () {
yield * this.os.iterate(this, lowerBound, null, function * (val) {
expect(val).toBeDefined()
actualResults++
})
expect(expectedResults).toEqual(actualResults)
done()
})
})
it('iterating over a tree without bounds yield the right amount of results', function (done) {
var lowerBound = null
var expectedResults = elements.filter(function (e, pos) {
return elements.indexOf(e) === pos
}).length
var actualResults = 0
store.requestTransaction(function * () {
yield * this.os.iterate(this, lowerBound, null, function * (val) {
expect(val).toBeDefined()
actualResults++
})
expect(expectedResults).toEqual(actualResults)
done()
})
})
it('iterating over a tree with upper bound yields the right amount of results', function (done) {
var upperBound = elements[Math.floor(Math.random() * elements.length)]
var expectedResults = elements.filter(function (e, pos) {
return (Y.utils.smaller(e, upperBound) || Y.utils.compareIds(e, upperBound)) && elements.indexOf(e) === pos
}).length
var actualResults = 0
store.requestTransaction(function * () {
yield * this.os.iterate(this, null, upperBound, function * (val) {
expect(val).toBeDefined()
actualResults++
})
expect(expectedResults).toEqual(actualResults)
done()
})
})
it('iterating over a tree with upper and lower bounds yield the right amount of results', function (done) {
var b1 = elements[Math.floor(Math.random() * elements.length)]
var b2 = elements[Math.floor(Math.random() * elements.length)]
var upperBound, lowerBound
if (Y.utils.smaller(b1, b2)) {
lowerBound = b1
upperBound = b2
} else {
lowerBound = b2
upperBound = b1
}
var expectedResults = elements.filter(function (e, pos) {
return (Y.utils.smaller(lowerBound, e) || Y.utils.compareIds(e, lowerBound)) &&
(Y.utils.smaller(e, upperBound) || Y.utils.compareIds(e, upperBound)) && elements.indexOf(e) === pos
}).length
var actualResults = 0
store.requestTransaction(function * () {
yield * this.os.iterate(this, lowerBound, upperBound, function * (val) {
expect(val).toBeDefined()
actualResults++
})
expect(expectedResults).toEqual(actualResults)
done()
})
})
})
})
})
}

View File

@@ -7,37 +7,50 @@ export class BinaryEncoder {
constructor () {
this.data = []
}
get length () {
return this.data.length
}
get pos () {
return this.data.length
}
createBuffer () {
return Uint8Array.from(this.data).buffer
}
writeUint8 (num) {
this.data.push(num & bits8)
}
setUint8 (pos, num) {
this.data[pos] = num & bits8
}
writeUint16 (num) {
this.data.push(num & bits8, (num >>> 8) & bits8)
}
setUint16 (pos, num) {
this.data[pos] = num & bits8
this.data[pos + 1] = (num >>> 8) & bits8
}
writeUint32 (num) {
for (let i = 0; i < 4; i++) {
this.data.push(num & bits8)
num >>>= 8
}
}
setUint32 (pos, num) {
for (let i = 0; i < 4; i++) {
this.data[pos + i] = num & bits8
num >>>= 8
}
}
writeVarUint (num) {
while (num >= 0b10000000) {
this.data.push(0b10000000 | (bits7 & num))
@@ -45,6 +58,7 @@ export class BinaryEncoder {
}
this.data.push(bits7 & num)
}
writeVarString (str) {
let bytes = utf8.setBytesFromString(str)
let len = bytes.length
@@ -53,6 +67,7 @@ export class BinaryEncoder {
this.data.push(bytes[i])
}
}
writeOpID (id) {
let user = id[0]
this.writeVarUint(user)
@@ -68,19 +83,22 @@ export class BinaryDecoder {
constructor (buffer) {
if (buffer instanceof ArrayBuffer) {
this.uint8arr = new Uint8Array(buffer)
} else if (buffer instanceof Uint8Array) {
} else if (buffer instanceof Uint8Array || (typeof Buffer !== 'undefined' && buffer instanceof Buffer)) {
this.uint8arr = buffer
} else {
throw new Error('Expected an ArrayBuffer or Uint8Array!')
}
this.pos = 0
}
skip8 () {
this.pos++
}
readUint8 () {
return this.uint8arr[this.pos++]
}
readUint32 () {
let uint =
this.uint8arr[this.pos] +
@@ -90,9 +108,11 @@ export class BinaryDecoder {
this.pos += 4
return uint
}
peekUint8 () {
return this.uint8arr[this.pos]
}
readVarUint () {
let num = 0
let len = 0
@@ -108,6 +128,7 @@ export class BinaryDecoder {
}
}
}
readVarString () {
let len = this.readVarUint()
let bytes = new Array(len)
@@ -116,12 +137,14 @@ export class BinaryDecoder {
}
return utf8.getStringFromBytes(bytes)
}
peekVarString () {
let pos = this.pos
let s = this.readVarString()
this.pos = pos
return s
}
readOpID () {
let user = this.readVarUint()
if (user !== 0xFFFFFF) {

View File

@@ -34,7 +34,7 @@ export function logMessageUpdate (decoder, strBuilder) {
}
export function computeMessageUpdate (decoder, encoder, conn) {
if (conn.y.db.forwardAppliedOperations) {
if (conn.y.db.forwardAppliedOperations || conn.y.persistence != null) {
let messagePosition = decoder.pos
let len = decoder.readUint32()
let delops = []
@@ -45,7 +45,12 @@ export function computeMessageUpdate (decoder, encoder, conn) {
}
}
if (delops.length > 0) {
conn.broadcastOps(delops)
if (conn.y.db.forwardAppliedOperations) {
conn.broadcastOps(delops)
}
if (conn.y.persistence) {
conn.y.persistence.saveOperations(delops)
}
}
decoder.pos = messagePosition
}
@@ -53,7 +58,7 @@ export function computeMessageUpdate (decoder, encoder, conn) {
}
export function sendSyncStep1 (conn, syncUser) {
conn.y.db.requestTransaction(function * () {
conn.y.db.requestTransaction(function () {
let encoder = new BinaryEncoder()
encoder.writeVarString(conn.opts.room || '')
encoder.writeVarString('sync step 1')
@@ -61,7 +66,7 @@ export function sendSyncStep1 (conn, syncUser) {
encoder.writeVarUint(conn.protocolVersion)
let preferUntransformed = conn.preferUntransformed && this.os.length === 0 // TODO: length may not be defined
encoder.writeUint8(preferUntransformed ? 1 : 0)
yield * this.writeStateSet(encoder)
this.writeStateSet(encoder)
conn.send(syncUser, encoder.createBuffer())
})
}
@@ -92,27 +97,30 @@ export function computeMessageSyncStep1 (decoder, encoder, conn, senderConn, sen
conn.y.destroy()
}
// send sync step 2
conn.y.db.requestTransaction(function * () {
encoder.writeVarString('sync step 2')
encoder.writeVarString(conn.authInfo || '')
return conn.y.db.whenTransactionsFinished().then(() => {
// send sync step 2
conn.y.db.requestTransaction(function () {
encoder.writeVarString('sync step 2')
encoder.writeVarString(conn.authInfo || '')
if (preferUntransformed) {
encoder.writeUint8(1)
yield * this.writeOperationsUntransformed(encoder)
} else {
encoder.writeUint8(0)
yield * this.writeOperations(encoder, decoder)
}
if (preferUntransformed) {
encoder.writeUint8(1)
this.writeOperationsUntransformed(encoder)
} else {
encoder.writeUint8(0)
this.writeOperations(encoder, decoder)
}
yield * this.writeDeleteSet(encoder)
conn.send(senderConn.uid, encoder.createBuffer())
senderConn.receivedSyncStep2 = true
this.writeDeleteSet(encoder)
conn.send(senderConn.uid, encoder.createBuffer())
senderConn.receivedSyncStep2 = true
})
return conn.y.db.whenTransactionsFinished().then(() => {
if (conn.role === 'slave') {
sendSyncStep1(conn, sender)
}
})
})
if (conn.role === 'slave') {
sendSyncStep1(conn, sender)
}
return conn.y.db.whenTransactionsFinished()
}
export function logSS (decoder, strBuilder) {
@@ -166,17 +174,17 @@ export function computeMessageSyncStep2 (decoder, encoder, conn, senderConn, sen
let defer = senderConn.syncStep2
// apply operations first
db.requestTransaction(function * () {
db.requestTransaction(function () {
let osUntransformed = decoder.readUint8()
if (osUntransformed === 1) {
yield * this.applyOperationsUntransformed(decoder)
this.applyOperationsUntransformed(decoder)
} else {
this.store.applyOperations(decoder)
}
})
// then apply ds
db.requestTransaction(function * () {
yield * this.applyDeleteSet(decoder)
db.requestTransaction(function () {
this.applyDeleteSet(decoder)
})
return db.whenTransactionsFinished().then(() => {
conn._setSyncedWith(sender)

46
src/Persistence.js Normal file
View File

@@ -0,0 +1,46 @@
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
}

506
src/RedBlackTree.js Normal file
View File

@@ -0,0 +1,506 @@
export default function extendRBTree (Y) {
class N {
// A created node is always red!
constructor (val) {
this.val = val
this.color = true
this._left = null
this._right = null
this._parent = null
if (val.id === null) {
throw new Error('You must define id!')
}
}
isRed () { return this.color }
isBlack () { return !this.color }
redden () { this.color = true; return this }
blacken () { this.color = false; return this }
get grandparent () {
return this.parent.parent
}
get parent () {
return this._parent
}
get sibling () {
return (this === this.parent.left)
? this.parent.right : this.parent.left
}
get left () {
return this._left
}
get right () {
return this._right
}
set left (n) {
if (n !== null) {
n._parent = this
}
this._left = n
}
set right (n) {
if (n !== null) {
n._parent = this
}
this._right = n
}
rotateLeft (tree) {
var parent = this.parent
var newParent = this.right
var newRight = this.right.left
newParent.left = this
this.right = newRight
if (parent === null) {
tree.root = newParent
newParent._parent = null
} else if (parent.left === this) {
parent.left = newParent
} else if (parent.right === this) {
parent.right = newParent
} else {
throw new Error('The elements are wrongly connected!')
}
}
next () {
if (this.right !== null) {
// search the most left node in the right tree
var o = this.right
while (o.left !== null) {
o = o.left
}
return o
} else {
var p = this
while (p.parent !== null && p !== p.parent.left) {
p = p.parent
}
return p.parent
}
}
prev () {
if (this.left !== null) {
// search the most right node in the left tree
var o = this.left
while (o.right !== null) {
o = o.right
}
return o
} else {
var p = this
while (p.parent !== null && p !== p.parent.right) {
p = p.parent
}
return p.parent
}
}
rotateRight (tree) {
var parent = this.parent
var newParent = this.left
var newLeft = this.left.right
newParent.right = this
this.left = newLeft
if (parent === null) {
tree.root = newParent
newParent._parent = null
} else if (parent.left === this) {
parent.left = newParent
} else if (parent.right === this) {
parent.right = newParent
} else {
throw new Error('The elements are wrongly connected!')
}
}
getUncle () {
// we can assume that grandparent exists when this is called!
if (this.parent === this.parent.parent.left) {
return this.parent.parent.right
} else {
return this.parent.parent.left
}
}
}
class RBTree {
constructor () {
this.root = null
this.length = 0
}
findNext (id) {
return this.findWithLowerBound([id[0], id[1] + 1])
}
findPrev (id) {
return this.findWithUpperBound([id[0], id[1] - 1])
}
findNodeWithLowerBound (from) {
if (from === void 0) {
throw new Error('You must define from!')
}
var o = this.root
if (o === null) {
return null
} else {
while (true) {
if ((from === null || Y.utils.smaller(from, o.val.id)) && o.left !== null) {
// o is included in the bound
// try to find an element that is closer to the bound
o = o.left
} else if (from !== null && Y.utils.smaller(o.val.id, from)) {
// o is not within the bound, maybe one of the right elements is..
if (o.right !== null) {
o = o.right
} else {
// there is no right element. Search for the next bigger element,
// this should be within the bounds
return o.next()
}
} else {
return o
}
}
}
}
findNodeWithUpperBound (to) {
if (to === void 0) {
throw new Error('You must define from!')
}
var o = this.root
if (o === null) {
return null
} else {
while (true) {
if ((to === null || Y.utils.smaller(o.val.id, to)) && o.right !== null) {
// o is included in the bound
// try to find an element that is closer to the bound
o = o.right
} else if (to !== null && Y.utils.smaller(to, o.val.id)) {
// o is not within the bound, maybe one of the left elements is..
if (o.left !== null) {
o = o.left
} else {
// there is no left element. Search for the prev smaller element,
// this should be within the bounds
return o.prev()
}
} else {
return o
}
}
}
}
findSmallestNode () {
var o = this.root
while (o != null && o.left != null) {
o = o.left
}
return o
}
findWithLowerBound (from) {
var n = this.findNodeWithLowerBound(from)
return n == null ? null : n.val
}
findWithUpperBound (to) {
var n = this.findNodeWithUpperBound(to)
return n == null ? null : n.val
}
iterate (t, from, to, f) {
var o
if (from === null) {
o = this.findSmallestNode()
} else {
o = this.findNodeWithLowerBound(from)
}
while (
o !== null &&
(
to === null || // eslint-disable-line no-unmodified-loop-condition
Y.utils.smaller(o.val.id, to) ||
Y.utils.compareIds(o.val.id, to)
)
) {
f.call(t, o.val)
o = o.next()
}
return true
}
logTable (from, to, filter) {
if (filter == null) {
filter = function () {
return true
}
}
if (from == null) { from = null }
if (to == null) { to = null }
var os = []
this.iterate(this, from, to, function (o) {
if (filter(o)) {
var o_ = {}
for (var key in o) {
if (typeof o[key] === 'object') {
o_[key] = JSON.stringify(o[key])
} else {
o_[key] = o[key]
}
}
os.push(o_)
}
})
if (console.table != null) {
console.table(os)
}
}
find (id) {
var n
return (n = this.findNode(id)) ? n.val : null
}
findNode (id) {
if (id == null || id.constructor !== Array) {
throw new Error('Expect id to be an array!')
}
var o = this.root
if (o === null) {
return false
} else {
while (true) {
if (o === null) {
return false
}
if (Y.utils.smaller(id, o.val.id)) {
o = o.left
} else if (Y.utils.smaller(o.val.id, id)) {
o = o.right
} else {
return o
}
}
}
}
delete (id) {
if (id == null || id.constructor !== Array) {
throw new Error('id is expected to be an Array!')
}
var d = this.findNode(id)
if (d == null) {
// throw new Error('Element does not exist!')
return
}
this.length--
if (d.left !== null && d.right !== null) {
// switch d with the greates element in the left subtree.
// o should have at most one child.
var o = d.left
// find
while (o.right !== null) {
o = o.right
}
// switch
d.val = o.val
d = o
}
// d has at most one child
// let n be the node that replaces d
var isFakeChild
var child = d.left || d.right
if (child === null) {
isFakeChild = true
child = new N({id: 0})
child.blacken()
d.right = child
} else {
isFakeChild = false
}
if (d.parent === null) {
if (!isFakeChild) {
this.root = child
child.blacken()
child._parent = null
} else {
this.root = null
}
return
} else if (d.parent.left === d) {
d.parent.left = child
} else if (d.parent.right === d) {
d.parent.right = child
} else {
throw new Error('Impossible!')
}
if (d.isBlack()) {
if (child.isRed()) {
child.blacken()
} else {
this._fixDelete(child)
}
}
this.root.blacken()
if (isFakeChild) {
if (child.parent.left === child) {
child.parent.left = null
} else if (child.parent.right === child) {
child.parent.right = null
} else {
throw new Error('Impossible #3')
}
}
}
_fixDelete (n) {
function isBlack (node) {
return node !== null ? node.isBlack() : true
}
function isRed (node) {
return node !== null ? node.isRed() : false
}
if (n.parent === null) {
// this can only be called after the first iteration of fixDelete.
return
}
// d was already replaced by the child
// d is not the root
// d and child are black
var sibling = n.sibling
if (isRed(sibling)) {
// make sibling the grandfather
n.parent.redden()
sibling.blacken()
if (n === n.parent.left) {
n.parent.rotateLeft(this)
} else if (n === n.parent.right) {
n.parent.rotateRight(this)
} else {
throw new Error('Impossible #2')
}
sibling = n.sibling
}
// parent, sibling, and children of n are black
if (n.parent.isBlack() &&
sibling.isBlack() &&
isBlack(sibling.left) &&
isBlack(sibling.right)
) {
sibling.redden()
this._fixDelete(n.parent)
} else if (n.parent.isRed() &&
sibling.isBlack() &&
isBlack(sibling.left) &&
isBlack(sibling.right)
) {
sibling.redden()
n.parent.blacken()
} else {
if (n === n.parent.left &&
sibling.isBlack() &&
isRed(sibling.left) &&
isBlack(sibling.right)
) {
sibling.redden()
sibling.left.blacken()
sibling.rotateRight(this)
sibling = n.sibling
} else if (n === n.parent.right &&
sibling.isBlack() &&
isRed(sibling.right) &&
isBlack(sibling.left)
) {
sibling.redden()
sibling.right.blacken()
sibling.rotateLeft(this)
sibling = n.sibling
}
sibling.color = n.parent.color
n.parent.blacken()
if (n === n.parent.left) {
sibling.right.blacken()
n.parent.rotateLeft(this)
} else {
sibling.left.blacken()
n.parent.rotateRight(this)
}
}
}
put (v) {
if (v == null || v.id == null || v.id.constructor !== Array) {
throw new Error('v is expected to have an id property which is an Array!')
}
var node = new N(v)
if (this.root !== null) {
var p = this.root // p abbrev. parent
while (true) {
if (Y.utils.smaller(node.val.id, p.val.id)) {
if (p.left === null) {
p.left = node
break
} else {
p = p.left
}
} else if (Y.utils.smaller(p.val.id, node.val.id)) {
if (p.right === null) {
p.right = node
break
} else {
p = p.right
}
} else {
p.val = node.val
return p
}
}
this._fixInsert(node)
} else {
this.root = node
}
this.length++
this.root.blacken()
return node
}
_fixInsert (n) {
if (n.parent === null) {
n.blacken()
return
} else if (n.parent.isBlack()) {
return
}
var uncle = n.getUncle()
if (uncle !== null && uncle.isRed()) {
// Note: parent: red, uncle: red
n.parent.blacken()
uncle.blacken()
n.grandparent.redden()
this._fixInsert(n.grandparent)
} else {
// Note: parent: red, uncle: black or null
// Now we transform the tree in such a way that
// either of these holds:
// 1) grandparent.left.isRed
// and grandparent.left.left.isRed
// 2) grandparent.right.isRed
// and grandparent.right.right.isRed
if (n === n.parent.right && n.parent === n.grandparent.left) {
n.parent.rotateLeft(this)
// Since we rotated and want to use the previous
// cases, we need to set n in such a way that
// n.parent.isRed again
n = n.left
} else if (n === n.parent.left && n.parent === n.grandparent.right) {
n.parent.rotateRight(this)
// see above
n = n.right
}
// Case 1) or 2) hold from here on.
// Now traverse grandparent, make parent a black node
// on the highest level which holds two red nodes.
n.parent.blacken()
n.grandparent.redden()
if (n === n.parent.left) {
// Case 1
n.grandparent.rotateRight(this)
} else {
// Case 2
n.grandparent.rotateLeft(this)
}
}
}
flush () {}
}
Y.utils.RBTree = RBTree
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,4 @@
import { BinaryEncoder, BinaryDecoder } from './Encoding.js'
/*
Partial definition of a transaction
@@ -85,11 +86,11 @@ export default function extendTransaction (Y) {
* does not check for Struct.*.requiredOps()
* also broadcasts it through the connector
*/
* applyCreatedOperations (ops) {
applyCreatedOperations (ops) {
var send = []
for (var i = 0; i < ops.length; i++) {
var op = ops[i]
yield * this.store.tryExecute.call(this, op)
this.store.tryExecute.call(this, op)
if (op.id == null || typeof op.id[1] !== 'string') {
send.push(Y.Struct[op.struct].encode(op))
}
@@ -97,20 +98,23 @@ export default function extendTransaction (Y) {
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
this.store.y.connector.broadcastOps(send)
if (this.store.y.persistence != null) {
this.store.y.persistence.saveOperations(send)
}
}
}
* deleteList (start) {
deleteList (start) {
while (start != null) {
start = yield * this.getOperation(start)
start = this.getOperation(start)
if (!start.gc) {
start.gc = true
start.deleted = true
yield * this.setOperation(start)
this.setOperation(start)
var delLength = start.content != null ? start.content.length : 1
yield * this.markDeleted(start.id, delLength)
this.markDeleted(start.id, delLength)
if (start.opContent != null) {
yield * this.deleteOperation(start.opContent)
this.deleteOperation(start.opContent)
}
this.store.queueGarbageCollector(start.id)
}
@@ -121,14 +125,14 @@ export default function extendTransaction (Y) {
/*
Mark an operation as deleted, and add it to the GC, if possible.
*/
* deleteOperation (targetId, length, preventCallType) /* :Generator<any, any, any> */ {
deleteOperation (targetId, length, preventCallType) /* :Generator<any, any, any> */ {
if (length == null) {
length = 1
}
yield * this.markDeleted(targetId, length)
this.markDeleted(targetId, length)
while (length > 0) {
var callType = false
var target = yield * this.os.findWithUpperBound([targetId[0], targetId[1] + length - 1])
var target = this.os.findWithUpperBound([targetId[0], targetId[1] + length - 1])
var targetLength = target != null && target.content != null ? target.content.length : 1
if (target == null || target.id[0] !== targetId[0] || target.id[1] + targetLength <= targetId[1]) {
// does not exist or is not in the range of the deletion
@@ -139,12 +143,12 @@ export default function extendTransaction (Y) {
if (!target.deleted) {
if (target.id[1] < targetId[1]) {
// starts to the left of the deletion range
target = yield * this.getInsertionCleanStart(targetId)
target = this.getInsertionCleanStart(targetId)
targetLength = target.content.length // must have content property!
}
if (target.id[1] + targetLength > targetId[1] + length) {
// ends to the right of the deletion range
target = yield * this.getInsertionCleanEnd([targetId[0], targetId[1] + length - 1])
target = this.getInsertionCleanEnd([targetId[0], targetId[1] + length - 1])
targetLength = target.content.length
}
}
@@ -159,35 +163,35 @@ export default function extendTransaction (Y) {
// delete containing lists
if (target.start != null) {
// TODO: don't do it like this .. -.-
yield * this.deleteList(target.start)
// yield * this.deleteList(target.id) -- do not gc itself because this may still get referenced
this.deleteList(target.start)
// this.deleteList(target.id) -- do not gc itself because this may still get referenced
}
if (target.map != null) {
for (var name in target.map) {
yield * this.deleteList(target.map[name])
this.deleteList(target.map[name])
}
// TODO: here to.. (see above)
// yield * this.deleteList(target.id) -- see above
// this.deleteList(target.id) -- see above
}
if (target.opContent != null) {
yield * this.deleteOperation(target.opContent)
this.deleteOperation(target.opContent)
// target.opContent = null
}
if (target.requires != null) {
for (var i = 0; i < target.requires.length; i++) {
yield * this.deleteOperation(target.requires[i])
this.deleteOperation(target.requires[i])
}
}
}
var left
if (target.left != null) {
left = yield * this.getInsertion(target.left)
left = this.getInsertion(target.left)
} else {
left = null
}
// set here because it was deleted and/or gc'd
yield * this.setOperation(target)
this.setOperation(target)
/*
Check if it is possible to add right to the gc.
@@ -196,12 +200,12 @@ export default function extendTransaction (Y) {
*/
var right
if (target.right != null) {
right = yield * this.getOperation(target.right)
right = this.getOperation(target.right)
} else {
right = null
}
if (callType && !preventCallType) {
yield * this.store.operationAdded(this, {
this.store.operationAdded(this, {
struct: 'Delete',
target: target.id,
length: targetLength,
@@ -209,9 +213,9 @@ export default function extendTransaction (Y) {
})
}
// need to gc in the end!
yield * this.store.addToGarbageCollector.call(this, target, left)
this.store.addToGarbageCollector.call(this, target, left)
if (right != null) {
yield * this.store.addToGarbageCollector.call(this, right, target)
this.store.addToGarbageCollector.call(this, right, target)
}
}
}
@@ -219,25 +223,25 @@ export default function extendTransaction (Y) {
/*
Mark an operation as deleted&gc'd
*/
* markGarbageCollected (id, len) {
markGarbageCollected (id, len) {
// this.mem.push(["gc", id]);
this.store.addToDebug('yield * this.markGarbageCollected(', id, ', ', len, ')')
var n = yield * this.markDeleted(id, len)
this.store.addToDebug('this.markGarbageCollected(', id, ', ', len, ')')
var n = this.markDeleted(id, len)
if (n.id[1] < id[1] && !n.gc) {
// un-extend left
var newlen = n.len - (id[1] - n.id[1])
n.len -= newlen
yield * this.ds.put(n)
this.ds.put(n)
n = {id: id, len: newlen, gc: false}
yield * this.ds.put(n)
this.ds.put(n)
}
// get prev&next before adding a new operation
var prev = yield * this.ds.findPrev(id)
var next = yield * this.ds.findNext(id)
var prev = this.ds.findPrev(id)
var next = this.ds.findNext(id)
if (id[1] + len < n.id[1] + n.len && !n.gc) {
// un-extend right
yield * this.ds.put({id: [id[0], id[1] + len], len: n.len - len, gc: false})
this.ds.put({id: [id[0], id[1] + len], len: n.len - len, gc: false})
n.len = len
}
// set gc'd
@@ -249,7 +253,7 @@ export default function extendTransaction (Y) {
Y.utils.compareIds([prev.id[0], prev.id[1] + prev.len], n.id)
) {
prev.len += n.len
yield * this.ds.delete(n.id)
this.ds.delete(n.id)
n = prev
// ds.put n here?
}
@@ -260,22 +264,22 @@ export default function extendTransaction (Y) {
Y.utils.compareIds([n.id[0], n.id[1] + n.len], next.id)
) {
n.len += next.len
yield * this.ds.delete(next.id)
this.ds.delete(next.id)
}
yield * this.ds.put(n)
yield * this.updateState(n.id[0])
this.ds.put(n)
this.updateState(n.id[0])
}
/*
Mark an operation as deleted.
returns the delete node
*/
* markDeleted (id, length) {
markDeleted (id, length) {
if (length == null) {
length = 1
}
// this.mem.push(["del", id]);
var n = yield * this.ds.findWithUpperBound(id)
var n = this.ds.findWithUpperBound(id)
if (n != null && n.id[0] === id[0]) {
if (n.id[1] <= id[1] && id[1] <= n.id[1] + n.len) {
// id is in n's range
@@ -289,7 +293,7 @@ export default function extendTransaction (Y) {
if (diff < length) {
// a partial deletion
n = {id: [id[0], id[1] + diff], len: length - diff, gc: false}
yield * this.ds.put(n)
this.ds.put(n)
} else {
// already gc'd
throw new Error(
@@ -304,15 +308,15 @@ export default function extendTransaction (Y) {
} else {
// cannot extend left (there is no left!)
n = {id: id, len: length, gc: false}
yield * this.ds.put(n) // TODO: you double-put !!
this.ds.put(n) // TODO: you double-put !!
}
} else {
// cannot extend left
n = {id: id, len: length, gc: false}
yield * this.ds.put(n)
this.ds.put(n)
}
// can extend right?
var next = yield * this.ds.findNext(n.id)
var next = this.ds.findNext(n.id)
if (
next != null &&
n.id[0] === next.id[0] &&
@@ -328,8 +332,8 @@ export default function extendTransaction (Y) {
// delete the missing range after next
diff = diff - next.len // missing range after next
if (diff > 0) {
yield * this.ds.put(n) // unneccessary? TODO!
yield * this.markDeleted([next.id[0], next.id[1] + next.len], diff)
this.ds.put(n) // unneccessary? TODO!
this.markDeleted([next.id[0], next.id[1] + next.len], diff)
}
}
break
@@ -338,8 +342,8 @@ export default function extendTransaction (Y) {
if (diff > next.len) {
// n is even longer than next
// get next.next, and try to extend it
var _next = yield * this.ds.findNext(next.id)
yield * this.ds.delete(next.id)
var _next = this.ds.findNext(next.id)
this.ds.delete(next.id)
if (_next == null || n.id[0] !== _next.id[0]) {
break
} else {
@@ -350,13 +354,13 @@ export default function extendTransaction (Y) {
} else {
// n just partially overlaps with next. extend n, delete next, and break this loop
n.len += next.len - diff
yield * this.ds.delete(next.id)
this.ds.delete(next.id)
break
}
}
}
}
yield * this.ds.put(n)
this.ds.put(n)
return n
}
/*
@@ -364,7 +368,7 @@ export default function extendTransaction (Y) {
other clients (e.g. master). This will query the database for
operations that can be gc'd and add them to the garbage collector.
*/
* garbageCollectAfterSync () {
garbageCollectAfterSync () {
// debugger
if (this.store.gc1.length > 0 || this.store.gc2.length > 0) {
console.warn('gc should be empty after sync')
@@ -372,28 +376,28 @@ export default function extendTransaction (Y) {
if (!this.store.gc) {
return
}
yield * this.os.iterate(this, null, null, function * (op) {
this.os.iterate(this, null, null, function (op) {
if (op.gc) {
delete op.gc
yield * this.setOperation(op)
this.setOperation(op)
}
if (op.parent != null) {
var parentDeleted = yield * this.isDeleted(op.parent)
var parentDeleted = this.isDeleted(op.parent)
if (parentDeleted) {
op.gc = true
if (!op.deleted) {
yield * this.markDeleted(op.id, op.content != null ? op.content.length : 1)
this.markDeleted(op.id, op.content != null ? op.content.length : 1)
op.deleted = true
if (op.opContent != null) {
yield * this.deleteOperation(op.opContent)
this.deleteOperation(op.opContent)
}
if (op.requires != null) {
for (var i = 0; i < op.requires.length; i++) {
yield * this.deleteOperation(op.requires[i])
this.deleteOperation(op.requires[i])
}
}
}
yield * this.setOperation(op)
this.setOperation(op)
this.store.gc1.push(op.id) // this is ok becaues its shortly before sync (otherwise use queueGarbageCollector!)
return
}
@@ -401,9 +405,9 @@ export default function extendTransaction (Y) {
if (op.deleted) {
var left = null
if (op.left != null) {
left = yield * this.getInsertion(op.left)
left = this.getInsertion(op.left)
}
yield * this.store.addToGarbageCollector.call(this, op, left)
this.store.addToGarbageCollector.call(this, op, left)
}
})
}
@@ -416,10 +420,10 @@ export default function extendTransaction (Y) {
* reset parent.end
* reset origins of all right ops
*/
* garbageCollectOperation (id) {
this.store.addToDebug('yield * this.garbageCollectOperation(', id, ')')
var o = yield * this.getOperation(id)
yield * this.markGarbageCollected(id, (o != null && o.content != null) ? o.content.length : 1) // always mark gc'd
garbageCollectOperation (id) {
this.store.addToDebug('this.garbageCollectOperation(', id, ')')
var o = this.getOperation(id)
this.markGarbageCollected(id, (o != null && o.content != null) ? o.content.length : 1) // always mark gc'd
// if op exists, then clean that mess up..
if (o != null) {
var deps = []
@@ -430,32 +434,32 @@ export default function extendTransaction (Y) {
deps = deps.concat(o.requires)
}
for (var i = 0; i < deps.length; i++) {
var dep = yield * this.getOperation(deps[i])
var dep = this.getOperation(deps[i])
if (dep != null) {
if (!dep.deleted) {
yield * this.deleteOperation(dep.id)
dep = yield * this.getOperation(dep.id)
this.deleteOperation(dep.id)
dep = this.getOperation(dep.id)
}
dep.gc = true
yield * this.setOperation(dep)
this.setOperation(dep)
this.store.queueGarbageCollector(dep.id)
} else {
yield * this.markGarbageCollected(deps[i], 1)
this.markGarbageCollected(deps[i], 1)
}
}
// remove gc'd op from the left op, if it exists
if (o.left != null) {
var left = yield * this.getInsertion(o.left)
var left = this.getInsertion(o.left)
left.right = o.right
yield * this.setOperation(left)
this.setOperation(left)
}
// remove gc'd op from the right op, if it exists
// also reset origins of right ops
if (o.right != null) {
var right = yield * this.getOperation(o.right)
var right = this.getOperation(o.right)
right.left = o.left
yield * this.setOperation(right)
this.setOperation(right)
if (o.originOf != null && o.originOf.length > 0) {
// find new origin of right ops
@@ -474,7 +478,7 @@ export default function extendTransaction (Y) {
right.origin = neworigin
// search until you find origin pointer to the left of o
if (right.right != null) {
var i = yield * this.getOperation(right.right)
var i = this.getOperation(right.right)
var ids = [o.id, o.right]
while (ids.some(function (id) {
return Y.utils.compareIds(id, i.origin)
@@ -482,14 +486,14 @@ export default function extendTransaction (Y) {
if (Y.utils.compareIds(i.origin, o.id)) {
// reset origin of i
i.origin = neworigin
yield * this.setOperation(i)
this.setOperation(i)
}
// get next i
if (i.right == null) {
break
} else {
ids.push(i.id)
i = yield * this.getOperation(i.right)
i = this.getOperation(i.right)
}
}
}
@@ -498,20 +502,20 @@ export default function extendTransaction (Y) {
// ** Now the new implementation starts **
// reset neworigin of all originOf[*]
for (var _i in o.originOf) {
var originsIn = yield * this.getOperation(o.originOf[_i])
var originsIn = this.getOperation(o.originOf[_i])
if (originsIn != null) {
originsIn.origin = neworigin
yield * this.setOperation(originsIn)
this.setOperation(originsIn)
}
}
if (neworigin != null) {
var neworigin_ = yield * this.getInsertion(neworigin)
var neworigin_ = this.getInsertion(neworigin)
if (neworigin_.originOf == null) {
neworigin_.originOf = o.originOf
} else {
neworigin_.originOf = o.originOf.concat(neworigin_.originOf)
}
yield * this.setOperation(neworigin_)
this.setOperation(neworigin_)
}
// we don't need to set right here, because
// right should be in o.originOf => it is set it the previous for loop
@@ -520,15 +524,15 @@ export default function extendTransaction (Y) {
// o may originate in another operation.
// Since o is deleted, we have to reset o.origin's `originOf` property
if (o.origin != null) {
var origin = yield * this.getInsertion(o.origin)
var origin = this.getInsertion(o.origin)
origin.originOf = origin.originOf.filter(function (_id) {
return !Y.utils.compareIds(id, _id)
})
yield * this.setOperation(origin)
this.setOperation(origin)
}
var parent
if (o.parent != null) {
parent = yield * this.getOperation(o.parent)
parent = this.getOperation(o.parent)
}
// remove gc'd op from parent, if it exists
if (parent != null) {
@@ -555,38 +559,38 @@ export default function extendTransaction (Y) {
}
}
if (setParent) {
yield * this.setOperation(parent)
this.setOperation(parent)
}
}
// finally remove it from the os
yield * this.removeOperation(o.id)
this.removeOperation(o.id)
}
}
* checkDeleteStoreForState (state) {
var n = yield * this.ds.findWithUpperBound([state.user, state.clock])
checkDeleteStoreForState (state) {
var n = this.ds.findWithUpperBound([state.user, state.clock])
if (n != null && n.id[0] === state.user && n.gc) {
state.clock = Math.max(state.clock, n.id[1] + n.len)
}
}
* updateState (user) {
var state = yield * this.getState(user)
yield * this.checkDeleteStoreForState(state)
var o = yield * this.getInsertion([user, state.clock])
updateState (user) {
var state = this.getState(user)
this.checkDeleteStoreForState(state)
var o = this.getInsertion([user, state.clock])
var oLength = (o != null && o.content != null) ? o.content.length : 1
while (o != null && user === o.id[0] && o.id[1] <= state.clock && o.id[1] + oLength > state.clock) {
// either its a new operation (1. case), or it is an operation that was deleted, but is not yet in the OS
state.clock += oLength
yield * this.checkDeleteStoreForState(state)
o = yield * this.os.findNext(o.id)
this.checkDeleteStoreForState(state)
o = this.os.findNext(o.id)
oLength = (o != null && o.content != null) ? o.content.length : 1
}
yield * this.setState(state)
this.setState(state)
}
/*
apply a delete set in order to get
the state of the supplied ds
*/
* applyDeleteSet (decoder) {
applyDeleteSet (decoder) {
var deletions = []
let dsLength = decoder.readUint32()
@@ -602,7 +606,7 @@ export default function extendTransaction (Y) {
}
var pos = 0
var d = dv[pos]
yield * this.ds.iterate(this, [user, 0], [user, Number.MAX_VALUE], function * (n) {
this.ds.iterate(this, [user, 0], [user, Number.MAX_VALUE], function (n) {
// cases:
// 1. d deletes something to the right of n
// => go to next n (break)
@@ -651,14 +655,14 @@ export default function extendTransaction (Y) {
for (var i = 0; i < deletions.length; i++) {
var del = deletions[i]
// always try to delete..
yield * this.deleteOperation([del[0], del[1]], del[2])
this.deleteOperation([del[0], del[1]], del[2])
if (del[3]) {
// gc..
yield * this.markGarbageCollected([del[0], del[1]], del[2]) // always mark gc'd
this.markGarbageCollected([del[0], del[1]], del[2]) // always mark gc'd
// remove operation..
var counter = del[1] + del[2]
while (counter >= del[1]) {
var o = yield * this.os.findWithUpperBound([del[0], counter - 1])
var o = this.os.findWithUpperBound([del[0], counter - 1])
if (o == null) {
break
}
@@ -669,33 +673,38 @@ export default function extendTransaction (Y) {
}
if (o.id[1] + oLen > del[1] + del[2]) {
// overlaps right
o = yield * this.getInsertionCleanEnd([del[0], del[1] + del[2] - 1])
o = this.getInsertionCleanEnd([del[0], del[1] + del[2] - 1])
}
if (o.id[1] < del[1]) {
// overlaps left
o = yield * this.getInsertionCleanStart([del[0], del[1]])
o = this.getInsertionCleanStart([del[0], del[1]])
}
counter = o.id[1]
yield * this.garbageCollectOperation(o.id)
this.garbageCollectOperation(o.id)
}
}
if (this.store.forwardAppliedOperations) {
if (this.store.forwardAppliedOperations || this.store.y.persistence != null) {
var ops = []
ops.push({struct: 'Delete', target: [del[0], del[1]], length: del[2]})
this.store.y.connector.broadcastOps(ops)
if (this.store.forwardAppliedOperations) {
this.store.y.connector.broadcastOps(ops)
}
if (this.store.y.persistence != null) {
this.store.y.persistence.saveOperations(ops)
}
}
}
}
* isGarbageCollected (id) {
var n = yield * this.ds.findWithUpperBound(id)
isGarbageCollected (id) {
var n = this.ds.findWithUpperBound(id)
return n != null && n.id[0] === id[0] && id[1] < n.id[1] + n.len && n.gc
}
/*
A DeleteSet (ds) describes all the deleted ops in the OS
*/
* writeDeleteSet (encoder) {
writeDeleteSet (encoder) {
var ds = new Map()
yield * this.ds.iterate(this, null, null, function * (n) {
this.ds.iterate(this, null, null, function (n) {
var user = n.id[0]
var counter = n.id[1]
var len = n.len
@@ -722,24 +731,29 @@ export default function extendTransaction (Y) {
}
}
}
* isDeleted (id) {
var n = yield * this.ds.findWithUpperBound(id)
isDeleted (id) {
var n = this.ds.findWithUpperBound(id)
return n != null && n.id[0] === id[0] && id[1] < n.id[1] + n.len
}
* setOperation (op) {
yield * this.os.put(op)
setOperation (op) {
this.os.put(op)
return op
}
* addOperation (op) {
yield * this.os.put(op)
addOperation (op) {
this.os.put(op)
// case op is created by this user, op is already broadcasted in applyCreatedOperations
if (op.id[0] !== this.store.userId && this.store.forwardAppliedOperations && typeof op.id[1] !== 'string') {
// is connected, and this is not going to be send in addOperation
this.store.y.connector.broadcastOps([op])
if (op.id[0] !== this.store.userId && typeof op.id[1] !== 'string') {
if (this.store.forwardAppliedOperations) {
// is connected, and this is not going to be send in addOperation
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)
* tryCombineWithLeft (op) {
tryCombineWithLeft (op) {
if (
op != null &&
op.left != null &&
@@ -747,7 +761,7 @@ export default function extendTransaction (Y) {
op.left[0] === op.id[0] &&
Y.utils.compareIds(op.left, op.origin)
) {
var left = yield * this.getInsertion(op.left)
var left = this.getInsertion(op.left)
if (left.content != null &&
left.id[1] + left.content.length === op.id[1] &&
left.originOf.length === 1 &&
@@ -762,13 +776,13 @@ export default function extendTransaction (Y) {
}
left.content = left.content.concat(op.content)
left.right = op.right
yield * this.os.delete(op.id)
yield * this.setOperation(left)
this.os.delete(op.id)
this.setOperation(left)
}
}
}
* getInsertion (id) {
var ins = yield * this.os.findWithUpperBound(id)
getInsertion (id) {
var ins = this.os.findWithUpperBound(id)
if (ins == null) {
return null
} else {
@@ -780,14 +794,14 @@ export default function extendTransaction (Y) {
}
}
}
* getInsertionCleanStartEnd (id) {
yield * this.getInsertionCleanStart(id)
return yield * this.getInsertionCleanEnd(id)
getInsertionCleanStartEnd (id) {
this.getInsertionCleanStart(id)
return this.getInsertionCleanEnd(id)
}
// Return an insertion such that id is the first element of content
// This function manipulates an operation, if necessary
* getInsertionCleanStart (id) {
var ins = yield * this.getInsertion(id)
getInsertionCleanStart (id) {
var ins = this.getInsertion(id)
if (ins != null) {
if (ins.id[1] === id[1]) {
return ins
@@ -801,8 +815,8 @@ export default function extendTransaction (Y) {
left.right = ins.id
ins.left = leftLid
// debugger // check
yield * this.setOperation(left)
yield * this.setOperation(ins)
this.setOperation(left)
this.setOperation(ins)
if (left.gc) {
this.store.queueGarbageCollector(ins.id)
}
@@ -814,8 +828,8 @@ export default function extendTransaction (Y) {
}
// Return an insertion such that id is the last element of content
// This function manipulates an operation, if necessary
* getInsertionCleanEnd (id) {
var ins = yield * this.getInsertion(id)
getInsertionCleanEnd (id) {
var ins = this.getInsertion(id)
if (ins != null) {
if (ins.content == null || (ins.id[1] + ins.content.length - 1 === id[1])) {
return ins
@@ -829,8 +843,8 @@ export default function extendTransaction (Y) {
ins.right = right.id
right.left = insLid
// debugger // check
yield * this.setOperation(right)
yield * this.setOperation(ins)
this.setOperation(right)
this.setOperation(ins)
if (ins.gc) {
this.store.queueGarbageCollector(right.id)
}
@@ -840,8 +854,8 @@ export default function extendTransaction (Y) {
return null
}
}
* getOperation (id/* :any */)/* :Transaction<any> */ {
var o = yield * this.os.find(id)
getOperation (id/* :any */)/* :Transaction<any> */ {
var o = this.os.find(id)
if (id[0] !== 0xFFFFFF || o != null) {
return o
} else { // type is string
@@ -849,9 +863,14 @@ export default function extendTransaction (Y) {
var comp = id[1].split('_')
if (comp.length > 1) {
var struct = comp[0]
var op = Y.Struct[struct].create(id)
let type = Y[comp[1]]
let args = null
if (type != null) {
args = Y.utils.parseTypeDefinition(type, comp[3])
}
var op = Y.Struct[struct].create(id, args)
op.type = comp[1]
yield * this.setOperation(op)
this.setOperation(op)
return op
} else {
throw new Error(
@@ -861,18 +880,18 @@ export default function extendTransaction (Y) {
}
}
}
* removeOperation (id) {
yield * this.os.delete(id)
removeOperation (id) {
this.os.delete(id)
}
* setState (state) {
setState (state) {
var val = {
id: [state.user],
clock: state.clock
}
yield * this.ss.put(val)
this.ss.put(val)
}
* getState (user) {
var n = yield * this.ss.find([user])
getState (user) {
var n = this.ss.find([user])
var clock = n == null ? null : n.clock
if (clock == null) {
clock = 0
@@ -882,9 +901,9 @@ export default function extendTransaction (Y) {
clock: clock
}
}
* getStateVector () {
getStateVector () {
var stateVector = []
yield * this.ss.iterate(this, null, null, function * (n) {
this.ss.iterate(this, null, null, function (n) {
stateVector.push({
user: n.id[0],
clock: n.clock
@@ -892,18 +911,18 @@ export default function extendTransaction (Y) {
})
return stateVector
}
* getStateSet () {
getStateSet () {
var ss = {}
yield * this.ss.iterate(this, null, null, function * (n) {
this.ss.iterate(this, null, null, function (n) {
ss[n.id[0]] = n.clock
})
return ss
}
* writeStateSet (encoder) {
writeStateSet (encoder) {
let lenPosition = encoder.pos
let len = 0
encoder.writeUint32(0)
yield * this.ss.iterate(this, null, null, function * (n) {
this.ss.iterate(this, null, null, function (n) {
encoder.writeVarUint(n.id[0])
encoder.writeVarUint(n.clock)
len++
@@ -957,14 +976,14 @@ export default function extendTransaction (Y) {
3. Found o = op.origin -> set op.left = op.origin, and send it to the user. start again from 1. (set op = o)
4. Found some o -> set o.right = op, o.left = o.origin, send it to the user, continue
*/
* getOperations (startSS) {
getOperations (startSS) {
// TODO: use bounds here!
if (startSS == null) {
startSS = new Map()
}
var send = []
var endSV = yield * this.getStateVector()
var endSV = this.getStateVector()
for (let endState of endSV) {
let user = endState.user
if (user === 0xFFFFFF) {
@@ -974,7 +993,7 @@ export default function extendTransaction (Y) {
if (startPos > 0) {
// There is a change that [user, startPos] is in a composed Insertion (with a smaller counter)
// find out if that is the case
let firstMissing = yield * this.getInsertion([user, startPos])
let firstMissing = this.getInsertion([user, startPos])
if (firstMissing != null) {
// update startPos
startPos = firstMissing.id[1]
@@ -988,7 +1007,7 @@ export default function extendTransaction (Y) {
if (user === 0xFFFFFF) {
continue
}
yield * this.os.iterate(this, [user, startPos], [user, Number.MAX_VALUE], function * (op) {
this.os.iterate(this, [user, startPos], [user, Number.MAX_VALUE], function (op) {
op = Y.Struct[op.struct].encode(op)
if (op.struct !== 'Insert') {
send.push(op)
@@ -1019,7 +1038,7 @@ export default function extendTransaction (Y) {
*/
break
}
o = yield * this.getInsertion(o.left)
o = this.getInsertion(o.left)
// we set another o, check if we can reduce $missingOrigins
while (missingOrigins.length > 0 && Y.utils.matchesId(o, missingOrigins[missingOrigins.length - 1].origin)) {
missingOrigins.pop()
@@ -1057,7 +1076,7 @@ export default function extendTransaction (Y) {
return send.reverse()
}
* writeOperations (encoder, decoder) {
writeOperations (encoder, decoder) {
let ss = new Map()
let ssLength = decoder.readUint32()
for (let i = 0; i < ssLength; i++) {
@@ -1065,55 +1084,69 @@ export default function extendTransaction (Y) {
let clock = decoder.readVarUint()
ss.set(user, clock)
}
let ops = yield * this.getOperations(ss)
let ops = this.getOperations(ss)
encoder.writeUint32(ops.length)
for (let i = 0; i < ops.length; i++) {
let op = ops[i]
Y.Struct[op.struct].binaryEncode(encoder, Y.Struct[op.struct].encode(op))
}
}
toBinary () {
let encoder = new BinaryEncoder()
this.writeOperationsUntransformed(encoder)
this.writeDeleteSet(encoder)
return encoder.createBuffer()
}
fromBinary (buffer) {
let decoder = new BinaryDecoder(buffer)
this.applyOperationsUntransformed(decoder)
this.applyDeleteSet(decoder)
}
/*
* Get the plain untransformed operations from the database.
* You can apply these operations using .applyOperationsUntransformed(ops)
*
*/
* writeOperationsUntransformed (encoder) {
writeOperationsUntransformed (encoder) {
let lenPosition = encoder.pos
let len = 0
encoder.writeUint32(0) // placeholder
yield * this.os.iterate(this, null, null, function * (op) {
this.os.iterate(this, null, null, function (op) {
if (op.id[0] !== 0xFFFFFF) {
len++
Y.Struct[op.struct].binaryEncode(encoder, Y.Struct[op.struct].encode(op))
}
})
encoder.setUint32(lenPosition, len)
yield * this.writeStateSet(encoder)
this.writeStateSet(encoder)
}
* applyOperationsUntransformed (decoder) {
applyOperationsUntransformed (decoder) {
let len = decoder.readUint32()
for (let i = 0; i < len; i++) {
let op = Y.Struct.binaryDecodeOperation(decoder)
yield * this.os.put(op)
this.os.put(op)
}
yield * this.os.iterate(this, null, null, function * (op) {
this.os.iterate(this, null, null, function (op) {
if (op.parent != null) {
if (op.struct === 'Insert') {
// update parents .map/start/end properties
if (op.parentSub != null && op.left == null) {
// op is child of Map
let parent = yield * this.getOperation(op.parent)
let parent = this.getOperation(op.parent)
parent.map[op.parentSub] = op.id
yield * this.setOperation(parent)
this.setOperation(parent)
} else if (op.right == null || op.left == null) {
let parent = yield * this.getOperation(op.parent)
let parent = this.getOperation(op.parent)
if (op.right == null) {
parent.end = Y.utils.getLastId(op)
}
if (op.left == null) {
parent.start = op.id
}
yield * this.setOperation(parent)
this.setOperation(parent)
}
}
}
@@ -1122,14 +1155,14 @@ export default function extendTransaction (Y) {
for (let i = 0; i < stateSetLength; i++) {
let user = decoder.readVarUint()
let clock = decoder.readVarUint()
yield * this.ss.put({
this.ss.put({
id: [user],
clock: clock
})
}
}
/* this is what we used before.. use this as a reference..
* makeOperationReady (startSS, op) {
makeOperationReady (startSS, op) {
op = Y.Struct[op.struct].encode(op)
op = Y.utils.copyObject(op) -- use copyoperation instead now!
var o = op
@@ -1139,7 +1172,7 @@ export default function extendTransaction (Y) {
// or the o that has no origin to the right of op
// (this is why we use the ids array)
while (o.right != null) {
var right = yield * this.getOperation(o.right)
var right = this.getOperation(o.right)
if (o.right[1] < (startSS[o.right[0]] || 0) || !ids.some(function (id) {
return Y.utils.compareIds(id, right.origin)
})) {
@@ -1153,10 +1186,10 @@ export default function extendTransaction (Y) {
return op
}
*/
* flush () {
yield * this.os.flush()
yield * this.ss.flush()
yield * this.ds.flush()
flush () {
this.os.flush()
this.ss.flush()
this.ds.flush()
}
}
Y.Transaction = TransactionInterface

View File

@@ -67,7 +67,11 @@ export default function Utils (Y) {
this._eventListener[name] = listener.filter(e => e !== f)
}
emit (name, value) {
(this._eventListener[name] || []).forEach(l => l(value))
let listener = this._eventListener[name] || []
if (name === 'error' && listener.length === 0) {
console.error(value)
}
listener.forEach(l => l(value))
}
destroy () {
this._eventListener = null
@@ -315,7 +319,7 @@ export default function Utils (Y) {
this.awaiting++
ops.map(Y.utils.copyOperation).forEach(this.onevent)
}
* awaitOps (transaction, f, args) {
awaitOps (transaction, f, args) {
function notSoSmartSort (array) {
// this function sorts insertions in a executable order
var result = []
@@ -339,7 +343,7 @@ export default function Utils (Y) {
}
var before = this.waiting.length
// somehow create new operations
yield * f.apply(transaction, args)
f.apply(transaction, args)
// remove all appended ops / awaited ops
this.waiting.splice(before)
if (this.awaiting > 0) this.awaiting--
@@ -349,7 +353,7 @@ export default function Utils (Y) {
for (let i = 0; i < this.waiting.length; i++) {
var o = this.waiting[i]
if (o.struct === 'Insert') {
var _o = yield * transaction.getInsertion(o.id)
var _o = transaction.getInsertion(o.id)
if (_o.parentSub != null && _o.left != null) {
// if o is an insertion of a map struc (parentSub is defined), then it shouldn't be necessary to compute left
this.waiting.splice(i, 1)
@@ -361,10 +365,10 @@ export default function Utils (Y) {
o.left = null
} else {
// find next undeleted op
var left = yield * transaction.getInsertion(_o.left)
var left = transaction.getInsertion(_o.left)
while (left.deleted != null) {
if (left.left != null) {
left = yield * transaction.getInsertion(left.left)
left = transaction.getInsertion(left.left)
} else {
left = null
break
@@ -690,7 +694,7 @@ export default function Utils (Y) {
this.writeBuffer = createEmptyOpsArray(5)
this.readBuffer = createEmptyOpsArray(10)
}
* find (id, noSuperCall) {
find (id, noSuperCall) {
var i, r
for (i = this.readBuffer.length - 1; i >= 0; i--) {
r = this.readBuffer[i]
@@ -716,7 +720,7 @@ export default function Utils (Y) {
if (i < 0 && noSuperCall === undefined) {
// did not reach break in last loop
// read id and put it to the end of readBuffer
o = yield * super.find(id)
o = super.find(id)
}
if (o != null) {
for (i = 0; i < this.readBuffer.length - 1; i++) {
@@ -726,7 +730,7 @@ export default function Utils (Y) {
}
return o
}
* put (o) {
put (o) {
var id = o.id
var i, r // helper variables
for (i = this.writeBuffer.length - 1; i >= 0; i--) {
@@ -746,7 +750,7 @@ export default function Utils (Y) {
// write writeBuffer[0]
var write = this.writeBuffer[0]
if (write.id[0] !== null) {
yield * super.put(write)
super.put(write)
}
// put o to the end of writeBuffer
for (i = 0; i < this.writeBuffer.length - 1; i++) {
@@ -766,7 +770,7 @@ export default function Utils (Y) {
}
this.readBuffer[this.readBuffer.length - 1] = o
}
* delete (id) {
delete (id) {
var i, r
for (i = 0; i < this.readBuffer.length; i++) {
r = this.readBuffer[i]
@@ -776,44 +780,44 @@ export default function Utils (Y) {
}
}
}
yield * this.flush()
yield * super.delete(id)
this.flush()
super.delete(id)
}
* findWithLowerBound (id) {
var o = yield * this.find(id, true)
findWithLowerBound (id) {
var o = this.find(id, true)
if (o != null) {
return o
} else {
yield * this.flush()
return yield * super.findWithLowerBound.apply(this, arguments)
this.flush()
return super.findWithLowerBound.apply(this, arguments)
}
}
* findWithUpperBound (id) {
var o = yield * this.find(id, true)
findWithUpperBound (id) {
var o = this.find(id, true)
if (o != null) {
return o
} else {
yield * this.flush()
return yield * super.findWithUpperBound.apply(this, arguments)
this.flush()
return super.findWithUpperBound.apply(this, arguments)
}
}
* findNext () {
yield * this.flush()
return yield * super.findNext.apply(this, arguments)
findNext () {
this.flush()
return super.findNext.apply(this, arguments)
}
* findPrev () {
yield * this.flush()
return yield * super.findPrev.apply(this, arguments)
findPrev () {
this.flush()
return super.findPrev.apply(this, arguments)
}
* iterate () {
yield * this.flush()
yield * super.iterate.apply(this, arguments)
iterate () {
this.flush()
super.iterate.apply(this, arguments)
}
* flush () {
flush () {
for (var i = 0; i < this.writeBuffer.length; i++) {
var write = this.writeBuffer[i]
if (write.id[0] !== null) {
yield * super.put(write)
super.put(write)
this.writeBuffer[i] = {
id: [null, null]
}
@@ -840,4 +844,17 @@ export default function Utils (Y) {
}
}
Y.utils.generateUserId = generateUserId
Y.utils.parseTypeDefinition = function parseTypeDefinition (type, typeArgs) {
var args = []
try {
args = JSON.parse('[' + typeArgs + ']')
} catch (e) {
throw new Error('Was not able to parse type definition!')
}
if (type.typeDefinition.parseArguments != null) {
args = type.typeDefinition.parseArguments(args[0])[1]
}
return args
}
}

67
src/y-memory.js Normal file
View File

@@ -0,0 +1,67 @@
import extendRBTree from './RedBlackTree'
export default function extend (Y) {
extendRBTree(Y)
class Transaction extends Y.Transaction {
constructor (store) {
super(store)
this.store = store
this.ss = store.ss
this.os = store.os
this.ds = store.ds
}
}
var Store = Y.utils.RBTree
var BufferedStore = Y.utils.createSmallLookupBuffer(Store)
class Database extends Y.AbstractDatabase {
constructor (y, opts) {
super(y, opts)
this.os = new BufferedStore()
this.ds = new Store()
this.ss = new BufferedStore()
}
logTable () {
var self = this
self.requestTransaction(function () {
console.log('User: ', this.store.y.connector.userId, "==============================") // eslint-disable-line
console.log("State Set (SS):", this.getStateSet()) // eslint-disable-line
console.log("Operation Store (OS):") // eslint-disable-line
this.os.logTable() // eslint-disable-line
console.log("Deletion Store (DS):") //eslint-disable-line
this.ds.logTable() // eslint-disable-line
if (this.store.gc1.length > 0 || this.store.gc2.length > 0) {
console.warn('GC1|2 not empty!', this.store.gc1, this.store.gc2)
}
if (JSON.stringify(this.store.listenersById) !== '{}') {
console.warn('listenersById not empty!')
}
if (JSON.stringify(this.store.listenersByIdExecuteNow) !== '[]') {
console.warn('listenersByIdExecuteNow not empty!')
}
if (this.store.transactionInProgress) {
console.warn('Transaction still in progress!')
}
}, true)
}
transact (makeGen) {
const t = new Transaction(this)
try {
while (makeGen != null) {
makeGen.call(t)
makeGen = this.getNextRequest()
}
} catch (e) {
this.y.emit('error', e)
}
}
destroy () {
super.destroy()
delete this.os
delete this.ss
delete this.ds
}
}
Y.memory = Database
}

View File

@@ -1,16 +1,20 @@
import extendConnector from './Connector.js'
import extendPersistence from './Persistence.js'
import extendDatabase from './Database.js'
import extendTransaction from './Transaction.js'
import extendStruct from './Struct.js'
import extendUtils from './Utils.js'
import extendMemory from './y-memory.js'
import debug from 'debug'
import { formatYjsMessage, formatYjsMessageType } from './MessageHandler.js'
extendConnector(Y)
extendPersistence(Y)
extendDatabase(Y)
extendTransaction(Y)
extendStruct(Y)
extendUtils(Y)
extendMemory(Y)
Y.debug = debug
debug.formatters.Y = formatYjsMessage
@@ -137,10 +141,23 @@ export default function Y (opts/* :YOptions */) /* :Promise<YConfig> */ {
opts.share = Y.utils.copyObject(opts.share)
Y.requestModules(modules).then(function () {
var yconfig = new YConfig(opts)
let resolved = false
if (opts.timeout != null && opts.timeout >= 0) {
setTimeout(function () {
if (!resolved) {
reject(new Error('Yjs init timeout'))
yconfig.destroy()
}
}, opts.timeout)
}
if (yconfig.persistence != null) {
yconfig.persistence.retrieveContent()
}
yconfig.db.whenUserIdSet(function () {
yconfig.init(function () {
resolved = true
resolve(yconfig)
})
}, reject)
})
}).catch(reject)
}
@@ -159,38 +176,35 @@ class YConfig extends Y.utils.NamedEventHandler {
this.options = opts
this.db = new Y[opts.db.name](this, opts.db)
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
}
init (callback) {
var opts = this.options
var share = {}
this.share = share
this.db.requestTransaction(function * requestTransaction () {
this.db.requestTransaction(function requestTransaction () {
// create shared object
for (var propertyname in opts.share) {
var typeConstructor = opts.share[propertyname].split('(')
let typeArgs = ''
if (typeConstructor.length === 2) {
typeArgs = typeConstructor[1].split(')')[0] || ''
}
var typeName = typeConstructor.splice(0, 1)
var type = Y[typeName]
var typedef = type.typeDefinition
var id = [0xFFFFFF, typedef.struct + '_' + typeName + '_' + propertyname + '_' + typeConstructor]
var args = []
if (typeConstructor.length === 1) {
try {
args = JSON.parse('[' + typeConstructor[0].split(')')[0] + ']')
} catch (e) {
throw new Error('Was not able to parse type definition! (share.' + propertyname + ')')
}
if (type.typeDefinition.parseArguments == null) {
throw new Error(typeName + ' does not expect arguments!')
} else {
args = typedef.parseArguments(args[0])[1]
}
}
share[propertyname] = yield * this.store.initType.call(this, id, args)
var id = [0xFFFFFF, typedef.struct + '_' + typeName + '_' + propertyname + '_' + typeArgs]
let args = Y.utils.parseTypeDefinition(type, typeArgs)
share[propertyname] = this.store.initType.call(this, id, args)
}
this.store.whenTransactionsFinished()
.then(callback)
})
this.db.whenTransactionsFinished()
.then(callback)
}
isConnected () {
return this.connector.isSynced
@@ -235,8 +249,8 @@ class YConfig extends Y.utils.NamedEventHandler {
return this.db.whenTransactionsFinished().then(function () {
self.db.destroyTypes()
// make sure to wait for all transactions before destroying the db
self.db.requestTransaction(function * () {
yield * self.db.destroy()
self.db.requestTransaction(function () {
self.db.destroy()
})
return self.db.whenTransactionsFinished()
})

View File

@@ -158,34 +158,12 @@ const writeList = Y.Struct.List.binaryEncode
const readList = Y.Struct.List.binaryDecode
test('encode/decode List operations', async function binList (t) {
testEncoding(t, writeList, readList, {
struct: 'List',
id: [100, 33],
type: 'Array'
})
t.log('info is an object')
testEncoding(t, writeList, readList, {
struct: 'List',
id: [100, 33],
type: 'Array',
info: { prop: 'yay' }
})
t.log('info is a string')
testEncoding(t, writeList, readList, {
struct: 'List',
id: [100, 33],
type: 'Array',
info: 'hi'
})
t.log('info is a number')
testEncoding(t, writeList, readList, {
struct: 'List',
id: [100, 33],
type: 'Array',
info: 400
start: null,
end: null
})
})
@@ -199,31 +177,4 @@ test('encode/decode Map operations', async function binMap (t) {
type: 'Map',
map: {}
})
t.log('info is an object')
testEncoding(t, writeMap, readMap, {
struct: 'Map',
id: [100, 33],
type: 'Map',
info: { prop: 'yay' },
map: {}
})
t.log('info is a string')
testEncoding(t, writeMap, readMap, {
struct: 'Map',
id: [100, 33],
type: 'Map',
map: {},
info: 'hi'
})
t.log('info is a number')
testEncoding(t, writeMap, readMap, {
struct: 'Map',
id: [100, 33],
type: 'Map',
map: {},
info: 400
})
})

217
test/red-black-tree.js Normal file
View File

@@ -0,0 +1,217 @@
import Y from '../src/y.js'
import Chance from 'chance'
import { test, proxyConsole } from 'cutest'
proxyConsole()
var numberOfRBTreeTests = 10000
function checkRedNodesDoNotHaveBlackChildren (t, tree) {
let correct = true
function traverse (n) {
if (n == null) {
return
}
if (n.isRed()) {
if (n.left != null) {
correct = correct && !n.left.isRed()
}
if (n.right != null) {
correct = correct && !n.right.isRed()
}
}
traverse(n.left)
traverse(n.right)
}
traverse(tree.root)
t.assert(correct, 'Red nodes do not have black children')
}
function checkBlackHeightOfSubTreesAreEqual (t, tree) {
let correct = true
function traverse (n) {
if (n == null) {
return 0
}
var sub1 = traverse(n.left)
var sub2 = traverse(n.right)
if (sub1 !== sub2) {
correct = false
}
if (n.isRed()) {
return sub1
} else {
return sub1 + 1
}
}
traverse(tree.root)
t.assert(correct, 'Black-height of sub-trees are equal')
}
function checkRootNodeIsBlack (t, tree) {
t.assert(tree.root == null || tree.root.isBlack(), 'root node is black')
}
test('RedBlack Tree', async function redBlackTree (t) {
let memory = new Y.memory(null, { // eslint-disable-line
name: 'Memory',
gcTimeout: -1
})
let tree = memory.os
memory.requestTransaction(function () {
tree.put({id: [8433]})
tree.put({id: [12844]})
tree.put({id: [1795]})
tree.put({id: [30302]})
tree.put({id: [64287]})
tree.delete([8433])
tree.put({id: [28996]})
tree.delete([64287])
tree.put({id: [22721]})
})
await memory.whenTransactionsFinished()
checkRootNodeIsBlack(t, tree)
checkBlackHeightOfSubTreesAreEqual(t, tree)
checkRedNodesDoNotHaveBlackChildren(t, tree)
})
test(`random tests (${numberOfRBTreeTests})`, async function random (t) {
let chance = new Chance(t.getSeed() * 1000000000)
let memory = new Y.memory(null, { // eslint-disable-line
name: 'Memory',
gcTimeout: -1
})
let tree = memory.os
let elements = []
memory.requestTransaction(function () {
for (var i = 0; i < numberOfRBTreeTests; i++) {
if (chance.bool({likelihood: 80})) {
// 80% chance to insert an element
let obj = [chance.integer({min: 0, max: numberOfRBTreeTests})]
let nodeExists = tree.find(obj)
if (!nodeExists) {
if (elements.some(e => e[0] === obj[0])) {
t.assert(false, 'tree and elements contain different results')
}
elements.push(obj)
tree.put({id: obj})
}
} else if (elements.length > 0) {
// ~20% chance to delete an element
var elem = chance.pickone(elements)
elements = elements.filter(function (e) {
return !Y.utils.compareIds(e, elem)
})
tree.delete(elem)
}
}
})
await memory.whenTransactionsFinished()
checkRootNodeIsBlack(t, tree)
checkBlackHeightOfSubTreesAreEqual(t, tree)
checkRedNodesDoNotHaveBlackChildren(t, tree)
memory.requestTransaction(function () {
let allNodesExist = true
for (let id of elements) {
let node = tree.find(id)
if (!Y.utils.compareIds(node.id, id)) {
allNodesExist = false
}
}
t.assert(allNodesExist, 'All inserted nodes exist')
})
memory.requestTransaction(function () {
let findAllNodesWithLowerBoundSerach = true
for (let id of elements) {
let node = tree.findWithLowerBound(id)
if (!Y.utils.compareIds(node.id, id)) {
findAllNodesWithLowerBoundSerach = false
}
}
t.assert(
findAllNodesWithLowerBoundSerach,
'Find every object with lower bound search'
)
})
memory.requestTransaction(function () {
let lowerBound = chance.pickone(elements)
let expectedResults = elements.filter((e, pos) =>
(Y.utils.smaller(lowerBound, e) || Y.utils.compareIds(e, lowerBound)) &&
elements.indexOf(e) === pos
).length
let actualResults = 0
tree.iterate(this, lowerBound, null, function (val) {
if (val == null) {
t.assert(false, 'val is undefined!')
}
actualResults++
})
t.assert(
expectedResults === actualResults,
'Iterating over a tree with lower bound yields the right amount of results'
)
})
memory.requestTransaction(function () {
let expectedResults = elements.filter((e, pos) =>
elements.indexOf(e) === pos
).length
let actualResults = 0
tree.iterate(this, null, null, function (val) {
if (val == null) {
t.assert(false, 'val is undefined!')
}
actualResults++
})
t.assert(
expectedResults === actualResults,
'iterating over a tree without bounds yields the right amount of results'
)
})
memory.requestTransaction(function () {
let upperBound = chance.pickone(elements)
let expectedResults = elements.filter((e, pos) =>
(Y.utils.smaller(e, upperBound) || Y.utils.compareIds(e, upperBound)) &&
elements.indexOf(e) === pos
).length
let actualResults = 0
tree.iterate(this, null, upperBound, function (val) {
if (val == null) {
t.assert(false, 'val is undefined!')
}
actualResults++
})
t.assert(
expectedResults === actualResults,
'iterating over a tree with upper bound yields the right amount of results'
)
})
memory.requestTransaction(function () {
let upperBound = chance.pickone(elements)
let lowerBound = chance.pickone(elements)
if (Y.utils.smaller(upperBound, lowerBound)) {
[lowerBound, upperBound] = [upperBound, lowerBound]
}
let expectedResults = elements.filter((e, pos) =>
(Y.utils.smaller(lowerBound, e) || Y.utils.compareIds(e, lowerBound)) &&
(Y.utils.smaller(e, upperBound) || Y.utils.compareIds(e, upperBound)) &&
elements.indexOf(e) === pos
).length
let actualResults = 0
tree.iterate(this, lowerBound, upperBound, function (val) {
if (val == null) {
t.assert(false, 'val is undefined!')
}
actualResults++
})
t.assert(
expectedResults === actualResults,
'iterating over a tree with upper bound yields the right amount of results'
)
})
await memory.whenTransactionsFinished()
})

374
test/y-array.tests.js Normal file
View File

@@ -0,0 +1,374 @@
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)
})
*/

371
test/y-map.tests.js Normal file
View File

@@ -0,0 +1,371 @@
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)
})
*/

352
test/y-xml.tests.js Normal file
View File

@@ -0,0 +1,352 @@
import { wait, initArrays, compareUsers, Y, flushAll, applyRandomTests } from '../../yjs/tests-lib/helper.js'
import { test } from 'cutest'
test('set property', async function xml0 (t) {
var { users, xml0, xml1 } = await initArrays(t, { users: 2 })
xml0.setAttribute('height', 10)
t.assert(xml0.getAttribute('height') === 10, 'Simple set+get works')
await flushAll(t, users)
t.assert(xml1.getAttribute('height') === 10, 'Simple set+get works (remote)')
await compareUsers(t, users)
})
test('events', async function xml1 (t) {
var { users, xml0, xml1 } = await initArrays(t, { users: 2 })
var event
var remoteEvent
let expectedEvent
xml0.observe(function (e) {
delete e._content
delete e.nodes
delete e.values
event = e
})
xml1.observe(function (e) {
delete e._content
delete e.nodes
delete e.values
remoteEvent = e
})
xml0.setAttribute('key', 'value')
expectedEvent = {
type: 'attributeChanged',
value: 'value',
name: 'key'
}
t.compare(event, expectedEvent, 'attribute changed event')
await flushAll(t, users)
t.compare(remoteEvent, expectedEvent, 'attribute changed event (remote)')
// check attributeRemoved
xml0.removeAttribute('key')
expectedEvent = {
type: 'attributeRemoved',
name: 'key'
}
t.compare(event, expectedEvent, 'attribute deleted event')
await flushAll(t, users)
t.compare(remoteEvent, expectedEvent, 'attribute deleted event (remote)')
// test childInserted event
expectedEvent = {
type: 'childInserted',
index: 0
}
xml0.insert(0, [Y.XmlText('some text')])
t.compare(event, expectedEvent, 'child inserted event')
await flushAll(t, users)
t.compare(remoteEvent, expectedEvent, 'child inserted event (remote)')
// test childRemoved
xml0.delete(0)
expectedEvent = {
type: 'childRemoved',
index: 0
}
t.compare(event, expectedEvent, 'child deleted event')
await flushAll(t, users)
t.compare(remoteEvent, expectedEvent, 'child deleted event (remote)')
await compareUsers(t, users)
})
test('attribute modifications (y -> dom)', async function xml2 (t) {
var { users, xml0 } = await initArrays(t, { users: 3 })
let dom0 = xml0.getDom()
xml0.setAttribute('height', '100px')
await wait()
t.assert(dom0.getAttribute('height') === '100px', 'setAttribute')
xml0.removeAttribute('height')
await wait()
t.assert(dom0.getAttribute('height') == null, 'removeAttribute')
xml0.setAttribute('class', 'stuffy stuff')
await wait()
t.assert(dom0.getAttribute('class') === 'stuffy stuff', 'set class attribute')
await compareUsers(t, users)
})
test('attribute modifications (dom -> y)', async function xml3 (t) {
var { users, xml0 } = await initArrays(t, { users: 3 })
let dom0 = xml0.getDom()
dom0.setAttribute('height', '100px')
await wait()
t.assert(xml0.getAttribute('height') === '100px', 'setAttribute')
dom0.removeAttribute('height')
await wait()
t.assert(xml0.getAttribute('height') == null, 'removeAttribute')
dom0.setAttribute('class', 'stuffy stuff')
await wait()
t.assert(xml0.getAttribute('class') === 'stuffy stuff', 'set class attribute')
await compareUsers(t, users)
})
test('element insert (dom -> y)', async function xml4 (t) {
var { users, xml0 } = await initArrays(t, { users: 3 })
let dom0 = xml0.getDom()
dom0.insertBefore(document.createTextNode('some text'), null)
dom0.insertBefore(document.createElement('p'), null)
await wait()
t.assert(xml0.get(0).toString() === 'some text', 'Retrieve Text Node')
t.assert(xml0.get(1).nodeName === 'P', 'Retrieve Element node')
await compareUsers(t, users)
})
test('element insert (y -> dom)', async function xml5 (t) {
var { users, xml0 } = await initArrays(t, { users: 3 })
let dom0 = xml0.getDom()
xml0.insert(0, [Y.XmlText('some text')])
xml0.insert(1, [Y.XmlElement('p')])
t.assert(dom0.childNodes[0].textContent === 'some text', 'Retrieve Text node')
t.assert(dom0.childNodes[1].nodeName === 'P', 'Retrieve Element node')
await compareUsers(t, users)
})
test('y on insert, then delete (dom -> y)', async function xml6 (t) {
var { users, xml0 } = await initArrays(t, { users: 3 })
let dom0 = xml0.getDom()
dom0.insertBefore(document.createElement('p'), null)
await wait()
t.assert(xml0.length === 1, 'one node present')
dom0.childNodes[0].remove()
await wait()
t.assert(xml0.length === 0, 'no node present after delete')
await compareUsers(t, users)
})
test('y on insert, then delete (y -> dom)', async function xml7 (t) {
var { users, xml0 } = await initArrays(t, { users: 3 })
let dom0 = xml0.getDom()
xml0.insert(0, [Y.XmlElement('p')])
t.assert(dom0.childNodes[0].nodeName === 'P', 'Get inserted element from dom')
xml0.delete(0, 1)
t.assert(dom0.childNodes.length === 0, '#childNodes is empty after delete')
await compareUsers(t, users)
})
test('delete consecutive (1) (Text)', async function xml8 (t) {
var { users, xml0 } = await initArrays(t, { users: 3 })
let dom0 = xml0.getDom()
xml0.insert(0, ['1', '2', '3'].map(Y.XmlText))
await wait()
xml0.delete(1, 2)
await wait()
t.assert(xml0.length === 1, 'check length (y)')
t.assert(dom0.childNodes.length === 1, 'check length (dom)')
t.assert(dom0.childNodes[0].textContent === '1', 'check content')
await compareUsers(t, users)
})
test('delete consecutive (2) (Text)', async function xml9 (t) {
var { users, xml0 } = await initArrays(t, { users: 3 })
let dom0 = xml0.getDom()
xml0.insert(0, ['1', '2', '3'].map(Y.XmlText))
await wait()
xml0.delete(0, 1)
xml0.delete(1, 1)
await wait()
t.assert(xml0.length === 1, 'check length (y)')
t.assert(dom0.childNodes.length === 1, 'check length (dom)')
t.assert(dom0.childNodes[0].textContent === '2', 'check content')
await compareUsers(t, users)
})
test('delete consecutive (1) (Element)', async function xml10 (t) {
var { users, xml0 } = await initArrays(t, { users: 3 })
let dom0 = xml0.getDom()
xml0.insert(0, [Y.XmlElement('A'), Y.XmlElement('B'), Y.XmlElement('C')])
await wait()
xml0.delete(1, 2)
await wait()
t.assert(xml0.length === 1, 'check length (y)')
t.assert(dom0.childNodes.length === 1, 'check length (dom)')
t.assert(dom0.childNodes[0].nodeName === 'A', 'check content')
await compareUsers(t, users)
})
test('delete consecutive (2) (Element)', async function xml11 (t) {
var { users, xml0 } = await initArrays(t, { users: 3 })
let dom0 = xml0.getDom()
xml0.insert(0, [Y.XmlElement('A'), Y.XmlElement('B'), Y.XmlElement('C')])
await wait()
xml0.delete(0, 1)
xml0.delete(1, 1)
await wait()
t.assert(xml0.length === 1, 'check length (y)')
t.assert(dom0.childNodes.length === 1, 'check length (dom)')
t.assert(dom0.childNodes[0].nodeName === 'B', 'check content')
await compareUsers(t, users)
})
test('Receive a bunch of elements (with disconnect)', async function xml12 (t) {
var { users, xml0, xml1 } = await initArrays(t, { users: 3 })
let dom0 = xml0.getDom()
let dom1 = xml1.getDom()
users[1].disconnect()
xml0.insert(0, [Y.XmlElement('A'), Y.XmlElement('B'), Y.XmlElement('C')])
xml0.insert(0, [Y.XmlElement('X'), Y.XmlElement('Y'), Y.XmlElement('Z')])
await users[1].reconnect()
await flushAll(t, users)
t.assert(xml0.length === 6, 'check length (y)')
t.assert(xml1.length === 6, 'check length (y) (reconnected user)')
t.assert(dom0.childNodes.length === 6, 'check length (dom)')
t.assert(dom1.childNodes.length === 6, 'check length (dom) (reconnected user)')
await compareUsers(t, users)
})
test('move element to a different position', async function xml13 (t) {
var { users, xml0, xml1 } = await initArrays(t, { users: 3 })
let dom0 = xml0.getDom()
let dom1 = xml1.getDom()
dom0.append(document.createElement('div'))
dom0.append(document.createElement('h1'))
await flushAll(t, users)
dom1.insertBefore(dom1.childNodes[0], null)
t.assert(dom1.childNodes[0].nodeName === 'H1', 'div was deleted (user 0)')
t.assert(dom1.childNodes[1].nodeName === 'DIV', 'div was moved to the correct position (user 0)')
t.assert(dom1.childNodes[0].nodeName === 'H1', 'div was deleted (user 1)')
t.assert(dom1.childNodes[1].nodeName === 'DIV', 'div was moved to the correct position (user 1)')
await compareUsers(t, users)
})
test('filter node', async function xml14 (t) {
var { users, xml0, xml1 } = await initArrays(t, { users: 3 })
let dom0 = xml0.getDom()
let dom1 = xml1.getDom()
let domFilter = (node, attrs) => {
if (node.nodeName === 'H1') {
return null
} else {
return attrs
}
}
xml0.setDomFilter(domFilter)
xml1.setDomFilter(domFilter)
dom0.append(document.createElement('div'))
dom0.append(document.createElement('h1'))
await flushAll(t, users)
t.assert(dom1.childNodes.length === 1, 'Only one node was not transmitted')
t.assert(dom1.childNodes[0].nodeName === 'DIV', 'div node was transmitted')
await compareUsers(t, users)
})
test('filter attribute', async function xml15 (t) {
var { users, xml0, xml1 } = await initArrays(t, { users: 3 })
let dom0 = xml0.getDom()
let dom1 = xml1.getDom()
let domFilter = (node, attrs) => {
return attrs.filter(name => name !== 'hidden')
}
xml0.setDomFilter(domFilter)
xml1.setDomFilter(domFilter)
dom0.setAttribute('hidden', 'true')
dom0.setAttribute('style', 'height: 30px')
dom0.setAttribute('data-me', '77')
await flushAll(t, users)
t.assert(dom0.getAttribute('hidden') === 'true', 'User 0 still has the attribute')
t.assert(dom1.getAttribute('hidden') == null, 'User 1 did not receive update')
t.assert(dom1.getAttribute('style') === 'height: 30px', 'User 1 received style update')
t.assert(dom1.getAttribute('data-me') === '77', 'User 1 received data update')
await compareUsers(t, users)
})
// TODO: move elements
var xmlTransactions = [
function attributeChange (t, user, chance) {
user.share.xml.getDom().setAttribute(chance.word(), chance.word())
},
function attributeChangeHidden (t, user, chance) {
user.share.xml.getDom().setAttribute('hidden', chance.word())
},
function insertText (t, user, chance) {
let dom = user.share.xml.getDom()
var succ = dom.children.length > 0 ? chance.pickone(dom.children) : null
dom.insertBefore(document.createTextNode(chance.word()), succ)
},
function insertHiddenDom (t, user, chance) {
let dom = user.share.xml.getDom()
var succ = dom.children.length > 0 ? chance.pickone(dom.children) : null
dom.insertBefore(document.createElement('hidden'), succ)
},
function insertDom (t, user, chance) {
let dom = user.share.xml.getDom()
var succ = dom.children.length > 0 ? chance.pickone(dom.children) : null
dom.insertBefore(document.createElement(chance.word()), succ)
},
function deleteChild (t, user, chance) {
let dom = user.share.xml.getDom()
if (dom.childNodes.length > 0) {
var d = chance.pickone(dom.childNodes)
d.remove()
}
},
function insertTextSecondLayer (t, user, chance) {
let dom = user.share.xml.getDom()
if (dom.children.length > 0) {
let dom2 = chance.pickone(dom.children)
let succ = dom2.childNodes.length > 0 ? chance.pickone(dom2.childNodes) : null
dom2.insertBefore(document.createTextNode(chance.word()), succ)
}
},
function insertDomSecondLayer (t, user, chance) {
let dom = user.share.xml.getDom()
if (dom.children.length > 0) {
let dom2 = chance.pickone(dom.children)
let succ = dom2.childNodes.length > 0 ? chance.pickone(dom2.childNodes) : null
dom2.insertBefore(document.createElement(chance.word()), succ)
}
},
function deleteChildSecondLayer (t, user, chance) {
let dom = user.share.xml.getDom()
if (dom.children.length > 0) {
let dom2 = chance.pickone(dom.children)
if (dom2.childNodes.length > 0) {
let d = chance.pickone(dom2.childNodes)
d.remove()
}
}
}
]
test('y-xml: Random tests (10)', async function xmlRandom10 (t) {
await applyRandomTests(t, xmlTransactions, 10)
})
test('y-xml: Random tests (42)', async function xmlRandom42 (t) {
await applyRandomTests(t, xmlTransactions, 42)
})
test('y-xml: Random tests (43)', async function xmlRandom43 (t) {
await applyRandomTests(t, xmlTransactions, 43)
})
test('y-xml: Random tests (44)', async function xmlRandom44 (t) {
await applyRandomTests(t, xmlTransactions, 44)
})
test('y-xml: Random tests (45)', async function xmlRandom45 (t) {
await applyRandomTests(t, xmlTransactions, 45)
})
test('y-xml: Random tests (46)', async function xmlRandom46 (t) {
await applyRandomTests(t, xmlTransactions, 46)
})
test('y-xml: Random tests (47)', async function xmlRandom47 (t) {
await applyRandomTests(t, xmlTransactions, 47)
})

View File

@@ -1,20 +1,24 @@
import _Y from '../../yjs/src/y.js'
import yMemory from '../../y-memory/src/y-memory.js'
import yArray from '../../y-array/src/y-array.js'
import yMap from '../../y-map/src/Map.js'
import yText from '../../y-text/src/y-text.js'
import yMap from '../../y-map/src/y-map.js'
import yXml from '../../y-xml/src/y-xml.js'
import yTest from './test-connector.js'
import Chance from 'chance'
export let Y = _Y
Y.extend(yMemory, yArray, yMap, yTest)
Y.extend(yArray, yText, yMap, yTest, yXml)
function * getStateSet () {
export var database = { name: 'memory' }
export var connector = { name: 'test', url: 'http://localhost:1234' }
function getStateSet () {
var ss = {}
yield * this.ss.iterate(this, null, null, function * (n) {
this.ss.iterate(this, null, null, function (n) {
var user = n.id[0]
var clock = n.clock
ss[user] = clock
@@ -22,9 +26,9 @@ function * getStateSet () {
return ss
}
function * getDeleteSet () {
function getDeleteSet () {
var ds = {}
yield * this.ds.iterate(this, null, null, function * (n) {
this.ds.iterate(this, null, null, function (n) {
var user = n.id[0]
var counter = n.id[1]
var len = n.len
@@ -44,6 +48,39 @@ export async function garbageCollectUsers (t, users) {
await Promise.all(users.map(u => u.db.emptyGarbageCollector()))
}
export function attrsObject (dom) {
let keys = []
let yxml = dom.__yxml
for (let i = 0; i < dom.attributes.length; i++) {
keys.push(dom.attributes[i].name)
}
keys = yxml._domFilter(dom, keys)
let obj = {}
for (let i = 0; i < keys.length; i++) {
let key = keys[i]
obj[key] = dom.getAttribute(key)
}
return obj
}
export function domToJson (dom) {
if (dom.nodeType === document.TEXT_NODE) {
return dom.textContent
} else if (dom.nodeType === document.ELEMENT_NODE) {
let attributes = attrsObject(dom, dom.__yxml)
let children = Array.from(dom.childNodes.values())
.filter(d => d.__yxml !== false)
.map(domToJson)
return {
name: dom.nodeName,
children: children,
attributes: attributes
}
} else {
throw new Error('Unsupported node type')
}
}
/*
* 1. reconnect and flush all
* 2. user 0 gc
@@ -60,7 +97,17 @@ export async function compareUsers (t, users) {
await wait()
await flushAll(t, users)
var userTypeContents = users.map(u => u.share.array._content.map(c => c.val || JSON.stringify(c.type)))
var userArrayValues = 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)
var userXmlValues = users.map(u => u.share.xml.getDom()).map(domToJson)
await users[0].db.garbageCollect()
await users[0].db.garbageCollect()
@@ -86,9 +133,9 @@ export async function compareUsers (t, users) {
let filterDeletedOps = users.every(u => u.db.gc === false)
var data = await Promise.all(users.map(async (u) => {
var data = {}
u.db.requestTransaction(function * () {
u.db.requestTransaction(function () {
let ops = []
yield * this.os.iterate(this, null, null, function * (op) {
this.os.iterate(this, null, null, function (op) {
ops.push(Y.Struct[op.struct].encode(op))
})
@@ -102,7 +149,7 @@ export async function compareUsers (t, users) {
as they might have been split up differently..
*/
if (filterDeletedOps) {
let opIsDeleted = yield * this.isDeleted(op.id)
let opIsDeleted = this.isDeleted(op.id)
if (!opIsDeleted) {
data.os[JSON.stringify(op.id)] = op
}
@@ -110,15 +157,18 @@ export async function compareUsers (t, users) {
data.os[JSON.stringify(op.id)] = op
}
}
data.ds = yield * getDeleteSet.apply(this)
data.ss = yield * getStateSet.apply(this)
data.ds = getDeleteSet.apply(this)
data.ss = getStateSet.apply(this)
})
await u.db.whenTransactionsFinished()
return data
}))
for (var i = 0; i < data.length - 1; i++) {
await t.asyncGroup(async () => {
t.compare(userTypeContents[i], userTypeContents[i + 1], 'types')
t.compare(userArrayValues[i], userArrayValues[i + 1], 'array 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(userXmlValues[i], userXmlValues[i + 1], 'xml types')
t.compare(data[i].os, data[i + 1].os, 'os')
t.compare(data[i].ds, data[i + 1].ds, 'ds')
t.compare(data[i].ss, data[i + 1].ss, 'ss')
@@ -133,19 +183,19 @@ export async function initArrays (t, opts) {
var result = {
users: []
}
var share = Object.assign({ flushHelper: 'Map', array: 'Array', map: 'Map' }, opts.share)
var share = Object.assign({ flushHelper: 'Map', array: 'Array', map: 'Map', xml: 'XmlElement("div")' }, opts.share)
var chance = opts.chance || new Chance(t.getSeed() * 1000000000)
var connector = Object.assign({ room: 'debugging_' + t.name, generateUserId: false, testContext: t, chance }, opts.connector)
var conn = Object.assign({ room: 'debugging_' + t.name, generateUserId: false, testContext: t, chance }, connector)
for (let i = 0; i < opts.users; i++) {
let dbOpts
let connOpts
if (i === 0) {
// Only one instance can gc!
dbOpts = Object.assign({ gc: false }, opts.db)
connOpts = Object.assign({ role: 'master' }, connector)
dbOpts = Object.assign({ gc: false }, database)
connOpts = Object.assign({ role: 'master' }, conn)
} else {
dbOpts = Object.assign({ gc: false }, opts.db)
connOpts = Object.assign({ role: 'slave' }, connector)
dbOpts = Object.assign({ gc: false }, database)
connOpts = Object.assign({ role: 'slave' }, conn)
}
let y = await Y({
connector: connOpts,
@@ -156,6 +206,13 @@ export async function initArrays (t, opts) {
for (let name in share) {
result[name + i] = y.share[name]
}
y.share.xml.setDomFilter(function (d, attrs) {
if (d.nodeName === 'HIDDEN') {
return null
} else {
return attrs.filter(a => a !== 'hidden')
}
})
}
result.array0.delete(0, result.array0.length)
if (result.users[0].connector.testRoom != null) {
@@ -220,3 +277,48 @@ export function wait (t) {
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
}

9
y.js Normal file

File diff suppressed because one or more lines are too long

1
y.js.map Normal file

File diff suppressed because one or more lines are too long

5630
y.node.js Normal file

File diff suppressed because it is too large Load Diff

1
y.node.js.map Normal file

File diff suppressed because one or more lines are too long

23576
y.test.js Normal file

File diff suppressed because one or more lines are too long

1
y.test.js.map Normal file

File diff suppressed because one or more lines are too long