Compare commits

..

16 Commits

Author SHA1 Message Date
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
Kevin Jahns
be3b8b65ce 13.0.0-11 2017-08-04 16:30:58 +02:00
Kevin Jahns
d093ef56c8 userJoined accepts auth parameter. Sync with all users at once, instead of one at a time 2017-08-04 16:27:07 +02:00
34 changed files with 2124 additions and 23620 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

@@ -1,5 +1,8 @@
/* global Y */
// eslint-disable-next-line
let search = new URLSearchParams(location.search)
// initialize a shared object. This function call returns a promise!
Y({
db: {
@@ -8,17 +11,20 @@ Y({
connector: {
name: 'websockets-client',
room: 'Textarea-example',
url: 'http://127.0.0.1:1234'
// url: '//localhost:1234',
url: 'https://yjs-v13.herokuapp.com/'
// options: { transports: ['websocket'], upgrade: false }
},
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..
}).catch(() => {
console.log('Something went wrong while creating the instance..')
})

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 yMap from '../../y-map/src/Map.js'
import yText from '../../y-text/src/Text.js'
import yXml from '../../y-xml/src/y-xml.js'
import yMemory from '../../y-memory/src/y-memory.js'
import yWebsocketsClient from '../../y-websockets-client/src/y-websockets-client.js'
Y.extend(yArray, yMap, yText, yXml, yMemory, yWebsocketsClient)
export default Y

2
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "yjs",
"version": "13.0.0-7",
"version": "13.0.0-16",
"lockfileVersion": 1,
"dependencies": {
"accepts": {

View File

@@ -1,6 +1,6 @@
{
"name": "yjs",
"version": "13.0.0-10",
"version": "13.0.0-16",
"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

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

View File

@@ -43,10 +43,12 @@ export default function extendConnector (Y/* :any */) {
this.setUserId(Y.utils.generateUserId())
}
}
reconnect () {
this.log('reconnecting..')
return this.y.db.startGarbageCollector()
}
disconnect () {
this.log('discronnecting..')
this.connections = new Map()
@@ -56,13 +58,16 @@ export default function extendConnector (Y/* :any */) {
this.y.db.stopGarbageCollector()
return this.y.db.whenTransactionsFinished()
}
repair () {
this.log('Repairing the state of Yjs. This can happen if messages get lost, and Yjs detects that something is wrong. If this happens often, please report an issue here: https://github.com/y-js/yjs/issues')
this.connections.forEach(user => { user.isSynced = false })
this.isSynced = false
this.currentSyncTarget = null
this.findNextSyncTarget()
this.connections.forEach((user, userId) => {
user.isSynced = false
this._syncWithUser(userId)
})
}
setUserId (userId) {
if (this.userId == null) {
if (!Number.isInteger(userId)) {
@@ -77,20 +82,21 @@ export default function extendConnector (Y/* :any */) {
return null
}
}
onUserEvent (f) {
this.userEventListeners.push(f)
}
removeUserEventListener (f) {
this.userEventListeners = this.userEventListeners.filter(g => f !== g)
}
userLeft (user) {
if (this.connections.has(user)) {
this.log('%s: User left %s', this.userId, user)
this.connections.delete(user)
if (user === this.currentSyncTarget) {
this.currentSyncTarget = null
this.findNextSyncTarget()
}
// check if isSynced event can be sent now
this._setSyncedWith(null)
for (var f of this.userEventListeners) {
f({
action: 'userLeft',
@@ -99,7 +105,7 @@ export default function extendConnector (Y/* :any */) {
}
}
}
userJoined (user, role) {
userJoined (user, role, auth) {
if (role == null) {
throw new Error('You must specify the role of the joined user!')
}
@@ -112,7 +118,7 @@ export default function extendConnector (Y/* :any */) {
isSynced: false,
role: role,
processAfterAuth: [],
auth: null,
auth: auth || null,
receivedSyncStep2: false
})
let defer = {}
@@ -125,9 +131,7 @@ export default function extendConnector (Y/* :any */) {
role: role
})
}
if (this.currentSyncTarget == null) {
this.findNextSyncTarget()
}
this._syncWithUser(user)
}
// Execute a function _when_ we are connected.
// If not connected, wait until connected
@@ -138,27 +142,11 @@ export default function extendConnector (Y/* :any */) {
this.whenSyncedListeners.push(f)
}
}
findNextSyncTarget () {
if (this.currentSyncTarget != null || this.role === 'slave') {
_syncWithUser (userid) {
if (this.role === 'slave') {
return // "The current sync has not finished or this is controlled by a master!"
}
var syncUser = null
for (var [uid, user] of this.connections) {
if (!user.isSynced) {
syncUser = uid
break
}
}
var conn = this
if (syncUser != null) {
this.currentSyncTarget = syncUser
sendSyncStep1(this, syncUser)
} else {
if (!conn.isSynced) {
conn._fireIsSyncedListeners()
}
}
sendSyncStep1(this, userid)
}
_fireIsSyncedListeners () {
this.y.db.whenTransactionsFinished().then(() => {
@@ -222,7 +210,8 @@ export default function extendConnector (Y/* :any */) {
/*
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!'))
}
@@ -239,7 +228,7 @@ export default function extendConnector (Y/* :any */) {
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!')
}
@@ -265,38 +254,37 @@ 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'))
}
}
_setSyncedWith (user) {
var conn = this.connections.get(user)
if (conn != null) {
conn.isSynced = true
if (user != null) {
this.connections.get(user).isSynced = true
}
if (user === this.currentSyncTarget) {
this.currentSyncTarget = null
this.findNextSyncTarget()
}
if (this.role === 'slave' && conn.role === 'master') {
let conns = Array.from(this.connections.values())
if (conns.length > 0 && conns.every(u => u.isSynced)) {
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

View File

@@ -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
}
@@ -588,7 +588,7 @@ 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 * () {

View File

@@ -7,37 +7,46 @@ export class BinaryEncoder {
constructor () {
this.data = []
}
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 +54,7 @@ export class BinaryEncoder {
}
this.data.push(bits7 & num)
}
writeVarString (str) {
let bytes = utf8.setBytesFromString(str)
let len = bytes.length
@@ -53,6 +63,7 @@ export class BinaryEncoder {
this.data.push(bytes[i])
}
}
writeOpID (id) {
let user = id[0]
this.writeVarUint(user)
@@ -68,19 +79,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 +104,11 @@ export class BinaryDecoder {
this.pos += 4
return uint
}
peekUint8 () {
return this.uint8arr[this.pos]
}
readVarUint () {
let num = 0
let len = 0
@@ -108,6 +124,7 @@ export class BinaryDecoder {
}
}
}
readVarString () {
let len = this.readVarUint()
let bytes = new Array(len)
@@ -116,12 +133,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
}
@@ -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)
yield * this.writeOperationsUntransformed(encoder)
} else {
encoder.writeUint8(0)
yield * this.writeOperations(encoder, decoder)
}
yield * this.writeDeleteSet(encoder)
conn.send(senderConn.uid, encoder.createBuffer())
senderConn.receivedSyncStep2 = true
yield * 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) {

43
src/Persistence.js Normal file
View File

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

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
@@ -97,6 +98,9 @@ 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)
}
}
}
@@ -679,10 +683,15 @@ export default function extendTransaction (Y) {
yield * 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)
}
}
}
}
@@ -733,9 +742,14 @@ export default function extendTransaction (Y) {
* addOperation (op) {
yield * 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)
@@ -849,7 +863,12 @@ 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)
return op
@@ -1072,6 +1091,20 @@ export default function extendTransaction (Y) {
Y.Struct[op.struct].binaryEncode(encoder, Y.Struct[op.struct].encode(op))
}
}
* toBinary () {
let encoder = new BinaryEncoder()
yield * this.writeOperationsUntransformed(encoder)
yield * this.writeDeleteSet(encoder)
return encoder.createBuffer()
}
* fromBinary (buffer) {
let decoder = new BinaryDecoder(buffer)
yield * this.applyOperationsUntransformed(decoder)
yield * this.applyDeleteSet(decoder)
}
/*
* Get the plain untransformed operations from the database.
* You can apply these operations using .applyOperationsUntransformed(ops)

View File

@@ -840,4 +840,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
}
}

View File

@@ -1,4 +1,5 @@
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'
@@ -7,6 +8,7 @@ import debug from 'debug'
import { formatYjsMessage, formatYjsMessageType } from './MessageHandler.js'
extendConnector(Y)
extendPersistence(Y)
extendDatabase(Y)
extendTransaction(Y)
extendStruct(Y)
@@ -137,10 +139,20 @@ 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)
}
yconfig.db.whenUserIdSet(function () {
yconfig.init(function () {
resolved = true
resolve(yconfig)
})
}, reject)
})
}).catch(reject)
}
@@ -159,6 +171,11 @@ 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) {
@@ -169,28 +186,26 @@ class YConfig extends Y.utils.NamedEventHandler {
// 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]
}
}
var id = [0xFFFFFF, typedef.struct + '_' + typeName + '_' + propertyname + '_' + typeArgs]
let args = Y.utils.parseTypeDefinition(type, typeArgs)
share[propertyname] = yield * this.store.initType.call(this, id, args)
}
this.store.whenTransactionsFinished()
.then(callback)
})
if (this.persistence != null) {
this.persistence.retrieveContent()
.then(() => this.db.whenTransactionsFinished())
.then(callback)
} else {
this.db.whenTransactionsFinished()
.then(callback)
}
}
isConnected () {
return this.connector.isSynced

View File

@@ -163,30 +163,6 @@ test('encode/decode List operations', async function binList (t) {
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
})
})
const writeMap = Y.Struct.Map.binaryEncode
@@ -199,31 +175,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
})
})

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)
})
*/

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

@@ -0,0 +1,288 @@
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)
})
// TODO: move elements
var xmlTransactions = [
function attributeChange (t, user, chance) {
user.share.xml.getDom().setAttribute(chance.word(), 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 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 randomXml10 (t) {
await applyRandomTests(t, xmlTransactions, 10)
})
test('y-xml: Random tests (42)', async function randomXml42 (t) {
await applyRandomTests(t, xmlTransactions, 42)
})
test('y-xml: Random tests (43)', async function randomXml43 (t) {
await applyRandomTests(t, xmlTransactions, 43)
})
test('y-xml: Random tests (44)', async function randomXml44 (t) {
await applyRandomTests(t, xmlTransactions, 44)
})
test('y-xml: Random tests (45)', async function randomXml45 (t) {
await applyRandomTests(t, xmlTransactions, 45)
})
test('y-xml: Random tests (46)', async function randomXml46 (t) {
await applyRandomTests(t, xmlTransactions, 46)
})
test('y-xml: Random tests (47)', async function randomXml47 (t) {
await applyRandomTests(t, xmlTransactions, 47)
})

View File

@@ -3,14 +3,19 @@ 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 yText from '../../y-text/src/Text.js'
import yMap from '../../y-map/src/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(yMemory, yArray, yText, yMap, yTest, yXml)
export var database = { name: 'memory' }
export var connector = { name: 'test', url: 'http://localhost:1234' }
function * getStateSet () {
var ss = {}
@@ -44,6 +49,31 @@ export async function garbageCollectUsers (t, users) {
await Promise.all(users.map(u => u.db.emptyGarbageCollector()))
}
export function attrsToObject (attrs) {
let obj = {}
for (var i = 0; i < attrs.length; i++) {
let attr = attrs[i]
obj[attr.name] = attr.value
}
return obj
}
export function domToJson (dom) {
if (dom.nodeType === document.TEXT_NODE) {
return dom.textContent
} else if (dom.nodeType === document.ELEMENT_NODE) {
let attributes = attrsToObject(dom.attributes)
let children = Array.from(dom.childNodes.values()).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 +90,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()
@@ -118,7 +158,10 @@ export async function compareUsers (t, users) {
}))
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 +176,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: true }, 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,
@@ -220,3 +263,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
}

10
y.js

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4956
y.node.js

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

17975
y.test.js

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long