From ab6cde07e6344c8d43de49316238f2cda65b97fe Mon Sep 17 00:00:00 2001 From: Kevin Jahns Date: Thu, 24 Aug 2017 14:44:23 +0200 Subject: [PATCH] Implemented Xml Struct --- .gitignore | 1 + examples/infiniteyjs/index.html | 58 ++ examples/infiniteyjs/index.js | 64 ++ examples/package.json | 4 + examples/rollup.config.js | 27 + examples/textarea/index.js | 8 +- examples/xml/index.html | 5 +- examples/xml/index.js | 2 + examples/yjs-dist.esm | 12 + package.json | 1 + rollup.test.js | 2 +- src/Database.js | 2 +- src/Struct.js | 1079 ++++++++++++++++--------------- src/Transaction.js | 7 +- src/Utils.js | 13 + src/y.js | 20 +- test/encode-decode.js | 51 -- test/y-xml.tests.js | 288 +++++++++ tests-lib/helper.js | 33 +- 19 files changed, 1087 insertions(+), 590 deletions(-) create mode 100644 examples/infiniteyjs/index.html create mode 100644 examples/infiniteyjs/index.js create mode 100644 examples/rollup.config.js create mode 100644 examples/yjs-dist.esm create mode 100644 test/y-xml.tests.js diff --git a/.gitignore b/.gitignore index 5a63f063..1173b99b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules bower_components /y.* +/examples/yjs-dist.js* diff --git a/examples/infiniteyjs/index.html b/examples/infiniteyjs/index.html new file mode 100644 index 00000000..dd4a67a9 --- /dev/null +++ b/examples/infiniteyjs/index.html @@ -0,0 +1,58 @@ + + + + +
+
+

Server 1 (disconnected)

+ +
+
+

Server 2 (disconnected)

+ +
+
+

Server 3 (disconnected)

+ +
+
+ + + + + + + + diff --git a/examples/infiniteyjs/index.js b/examples/infiniteyjs/index.js new file mode 100644 index 00000000..05e7dc85 --- /dev/null +++ b/examples/infiniteyjs/index.js @@ -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) + }) +}) diff --git a/examples/package.json b/examples/package.json index 2ea88274..c83710cf 100644 --- a/examples/package.json +++ b/examples/package.json @@ -2,6 +2,10 @@ "name": "examples", "version": "0.0.0", "description": "", + "scripts": { + "dist": "rollup -c", + "watch": "rollup -cw" + }, "author": "Kevin Jahns", "license": "MIT", "dependencies": { diff --git a/examples/rollup.config.js b/examples/rollup.config.js new file mode 100644 index 00000000..48838d84 --- /dev/null +++ b/examples/rollup.config.js @@ -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} + */ +` +} diff --git a/examples/textarea/index.js b/examples/textarea/index.js index d58ab9d1..c3d9e6c4 100644 --- a/examples/textarea/index.js +++ b/examples/textarea/index.js @@ -12,12 +12,12 @@ Y({ connector: { name: 'websockets-client', room: 'Textarea-example', - url: 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' } }).then(function (y) { window.yTextarea = y diff --git a/examples/xml/index.html b/examples/xml/index.html index ab8c93e5..4d3ff03a 100644 --- a/examples/xml/index.html +++ b/examples/xml/index.html @@ -1,8 +1,9 @@ - - + + + diff --git a/examples/xml/index.js b/examples/xml/index.js index 8a0f026e..46edfc05 100644 --- a/examples/xml/index.js +++ b/examples/xml/index.js @@ -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', diff --git a/examples/yjs-dist.esm b/examples/yjs-dist.esm new file mode 100644 index 00000000..2399227a --- /dev/null +++ b/examples/yjs-dist.esm @@ -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 diff --git a/package.json b/package.json index 572cfd7e..283accdf 100644 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/rollup.test.js b/rollup.test.js index c4df1298..c74129e6 100644 --- a/rollup.test.js +++ b/rollup.test.js @@ -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: [ diff --git a/src/Database.js b/src/Database.js index 7f904628..cb2f5601 100644 --- a/src/Database.js +++ b/src/Database.js @@ -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 * () { diff --git a/src/Struct.js b/src/Struct.js index ea03c042..8935a4d9 100644 --- a/src/Struct.js +++ b/src/Struct.js @@ -2,6 +2,7 @@ const CDELETE = 0 const CINSERT = 1 const CLIST = 2 const CMAP = 3 +const CXML = 4 /* An operation also defines the structure of a type. This is why operation and @@ -22,547 +23,597 @@ const CMAP = 3 - Operations that are required to execute this operation. */ export default function extendStruct (Y) { - var Struct = { - binaryDecodeOperation: function (decoder) { - let code = decoder.peekUint8() - if (code === CDELETE) { - return Y.Struct.Delete.binaryDecode(decoder) - } else if (code === CINSERT) { - return Y.Struct.Insert.binaryDecode(decoder) - } else if (code === CLIST) { - return Y.Struct.List.binaryDecode(decoder) - } else if (code === CMAP) { - return Y.Struct.Map.binaryDecode(decoder) - } else { - throw new Error('Unable to decode operation!') - } - }, - /* This is the only operation that is actually not a structure, because - it is not stored in the OS. This is why it _does not_ have an id - - op = { - target: Id + let Struct = {} + Y.Struct = Struct + Struct.binaryDecodeOperation = function (decoder) { + let code = decoder.peekUint8() + if (code === CDELETE) { + return Struct.Delete.binaryDecode(decoder) + } else if (code === CINSERT) { + return Struct.Insert.binaryDecode(decoder) + } else if (code === CLIST) { + return Struct.List.binaryDecode(decoder) + } else if (code === CMAP) { + return Struct.Map.binaryDecode(decoder) + } else if (code === CXML) { + return Struct.Xml.binaryDecode(decoder) + } else { + throw new Error('Unable to decode operation!') } - */ - Delete: { - encode: function (op) { - return { - target: op.target, - length: op.length || 0, - struct: 'Delete' - } - }, - binaryEncode: function (encoder, op) { - encoder.writeUint8(CDELETE) - encoder.writeOpID(op.target) - encoder.writeVarUint(op.length || 0) - }, - binaryDecode: function (decoder) { - decoder.skip8() - return { - target: decoder.readOpID(), - length: decoder.readVarUint(), - struct: 'Delete' - } - }, - requiredOps: function (op) { - return [] // [op.target] - }, - execute: function * (op) { - return yield * this.deleteOperation(op.target, op.length || 1) + } + + /* This is the only operation that is actually not a structure, because + it is not stored in the OS. This is why it _does not_ have an id + + op = { + target: Id + } + */ + Struct.Delete = { + encode: function (op) { + return { + target: op.target, + length: op.length || 0, + struct: 'Delete' } }, - Insert: { - /* { - content: [any], - opContent: Id, - id: Id, - left: Id, - origin: Id, - right: Id, - parent: Id, - parentSub: string (optional), // child of Map type - } - */ - encode: function (op/* :Insertion */) /* :Insertion */ { - // TODO: you could not send the "left" property, then you also have to - // "op.left = null" in $execute or $decode - var e/* :any */ = { - id: op.id, - left: op.left, - right: op.right, - origin: op.origin, - parent: op.parent, - struct: op.struct - } - if (op.parentSub != null) { - e.parentSub = op.parentSub - } - if (op.hasOwnProperty('opContent')) { - e.opContent = op.opContent - } else { - e.content = op.content.slice() - } + binaryEncode: function (encoder, op) { + encoder.writeUint8(CDELETE) + encoder.writeOpID(op.target) + encoder.writeVarUint(op.length || 0) + }, + binaryDecode: function (decoder) { + decoder.skip8() + return { + target: decoder.readOpID(), + length: decoder.readVarUint(), + struct: 'Delete' + } + }, + requiredOps: function (op) { + return [] // [op.target] + }, + execute: function * (op) { + return yield * this.deleteOperation(op.target, op.length || 1) + } + } - return e - }, - binaryEncode: function (encoder, op) { - encoder.writeUint8(CINSERT) - // compute info property - let contentIsText = op.content != null && op.content.every(c => typeof c === 'string' && c.length === 1) - let originIsLeft = Y.utils.compareIds(op.left, op.origin) - let info = - (op.parentSub != null ? 1 : 0) | - (op.opContent != null ? 2 : 0) | - (contentIsText ? 4 : 0) | - (originIsLeft ? 8 : 0) | - (op.left != null ? 16 : 0) | - (op.right != null ? 32 : 0) | - (op.origin != null ? 64 : 0) - encoder.writeUint8(info) - encoder.writeOpID(op.id) - encoder.writeOpID(op.parent) - if (info & 16) { - encoder.writeOpID(op.left) - } - if (info & 32) { - encoder.writeOpID(op.right) - } - if (!originIsLeft && info & 64) { - encoder.writeOpID(op.origin) - } - if (info & 1) { - // write parentSub - encoder.writeVarString(op.parentSub) - } - if (info & 2) { - // write opContent - encoder.writeOpID(op.opContent) - } else if (info & 4) { - // write text - encoder.writeVarString(op.content.join('')) - } else { - // convert to JSON and write - encoder.writeVarString(JSON.stringify(op.content)) - } - }, - binaryDecode: function (decoder) { - let op = { - struct: 'Insert' - } - decoder.skip8() - // get info property - let info = decoder.readUint8() + /* { + content: [any], + opContent: Id, + id: Id, + left: Id, + origin: Id, + right: Id, + parent: Id, + parentSub: string (optional), // child of Map type + } + */ + Struct.Insert = { + encode: function (op/* :Insertion */) /* :Insertion */ { + // TODO: you could not send the "left" property, then you also have to + // "op.left = null" in $execute or $decode + var e/* :any */ = { + id: op.id, + left: op.left, + right: op.right, + origin: op.origin, + parent: op.parent, + struct: op.struct + } + if (op.parentSub != null) { + e.parentSub = op.parentSub + } + if (op.hasOwnProperty('opContent')) { + e.opContent = op.opContent + } else { + e.content = op.content.slice() + } - op.id = decoder.readOpID() - op.parent = decoder.readOpID() - if (info & 16) { - op.left = decoder.readOpID() - } else { - op.left = null - } - if (info & 32) { - op.right = decoder.readOpID() - } else { - op.right = null - } - if (info & 8) { - // origin is left - op.origin = op.left - } else if (info & 64) { - op.origin = decoder.readOpID() - } else { - op.origin = null - } - if (info & 1) { - // has parentSub - op.parentSub = decoder.readVarString() - } - if (info & 2) { - // has opContent - op.opContent = decoder.readOpID() - } else if (info & 4) { - // has pure text content - op.content = decoder.readVarString().split('') - } else { - // has mixed content - let s = decoder.readVarString() - op.content = JSON.parse(s) - } - return op - }, - requiredOps: function (op) { - var ids = [] - if (op.left != null) { - ids.push(op.left) - } - if (op.right != null) { - ids.push(op.right) - } - if (op.origin != null && !Y.utils.compareIds(op.left, op.origin)) { - ids.push(op.origin) - } - // if (op.right == null && op.left == null) { - ids.push(op.parent) + return e + }, + binaryEncode: function (encoder, op) { + encoder.writeUint8(CINSERT) + // compute info property + let contentIsText = op.content != null && op.content.every(c => typeof c === 'string' && c.length === 1) + let originIsLeft = Y.utils.compareIds(op.left, op.origin) + let info = + (op.parentSub != null ? 1 : 0) | + (op.opContent != null ? 2 : 0) | + (contentIsText ? 4 : 0) | + (originIsLeft ? 8 : 0) | + (op.left != null ? 16 : 0) | + (op.right != null ? 32 : 0) | + (op.origin != null ? 64 : 0) + encoder.writeUint8(info) + encoder.writeOpID(op.id) + encoder.writeOpID(op.parent) + if (info & 16) { + encoder.writeOpID(op.left) + } + if (info & 32) { + encoder.writeOpID(op.right) + } + if (!originIsLeft && info & 64) { + encoder.writeOpID(op.origin) + } + if (info & 1) { + // write parentSub + encoder.writeVarString(op.parentSub) + } + if (info & 2) { + // write opContent + encoder.writeOpID(op.opContent) + } else if (info & 4) { + // write text + encoder.writeVarString(op.content.join('')) + } else { + // convert to JSON and write + encoder.writeVarString(JSON.stringify(op.content)) + } + }, + binaryDecode: function (decoder) { + let op = { + struct: 'Insert' + } + decoder.skip8() + // get info property + let info = decoder.readUint8() - if (op.opContent != null) { - ids.push(op.opContent) + op.id = decoder.readOpID() + op.parent = decoder.readOpID() + if (info & 16) { + op.left = decoder.readOpID() + } else { + op.left = null + } + if (info & 32) { + op.right = decoder.readOpID() + } else { + op.right = null + } + if (info & 8) { + // origin is left + op.origin = op.left + } else if (info & 64) { + op.origin = decoder.readOpID() + } else { + op.origin = null + } + if (info & 1) { + // has parentSub + op.parentSub = decoder.readVarString() + } + if (info & 2) { + // has opContent + op.opContent = decoder.readOpID() + } else if (info & 4) { + // has pure text content + op.content = decoder.readVarString().split('') + } else { + // has mixed content + let s = decoder.readVarString() + op.content = JSON.parse(s) + } + return op + }, + requiredOps: function (op) { + var ids = [] + if (op.left != null) { + ids.push(op.left) + } + if (op.right != null) { + ids.push(op.right) + } + if (op.origin != null && !Y.utils.compareIds(op.left, op.origin)) { + ids.push(op.origin) + } + // if (op.right == null && op.left == null) { + ids.push(op.parent) + + if (op.opContent != null) { + ids.push(op.opContent) + } + return ids + }, + getDistanceToOrigin: function * (op) { + if (op.left == null) { + return 0 + } else { + var d = 0 + var o = yield * this.getInsertion(op.left) + while (!Y.utils.matchesId(o, op.origin)) { + d++ + if (o.left == null) { + break + } else { + o = yield * this.getInsertion(o.left) + } } - return ids - }, - getDistanceToOrigin: function * (op) { - if (op.left == null) { - return 0 - } else { - var d = 0 - var o = yield * this.getInsertion(op.left) - while (!Y.utils.matchesId(o, op.origin)) { - d++ - if (o.left == null) { - break - } else { - o = yield * this.getInsertion(o.left) + return d + } + }, + /* + # $this has to find a unique position between origin and the next known character + # case 1: $origin equals $o.origin: the $creator parameter decides if left or right + # let $OL= [o1,o2,o3,o4], whereby $this is to be inserted between o1 and o4 + # o2,o3 and o4 origin is 1 (the position of o2) + # there is the case that $this.creator < o2.creator, but o3.creator < $this.creator + # then o2 knows o3. Since on another client $OL could be [o1,o3,o4] the problem is complex + # therefore $this would be always to the right of o3 + # case 2: $origin < $o.origin + # if current $this insert_position > $o origin: $this ins + # else $insert_position will not change + # (maybe we encounter case 1 later, then this will be to the right of $o) + # case 3: $origin > $o.origin + # $this insert_position is to the left of $o (forever!) + */ + execute: function * (op) { + var i // loop counter + + // during this function some ops may get split into two pieces (e.g. with getInsertionCleanEnd) + // We try to merge them later, if possible + var tryToRemergeLater = [] + + if (op.origin != null) { // TODO: !== instead of != + // we save in origin that op originates in it + // we need that later when we eventually garbage collect origin (see transaction) + var origin = yield * this.getInsertionCleanEnd(op.origin) + if (origin.originOf == null) { + origin.originOf = [] + } + origin.originOf.push(op.id) + yield * this.setOperation(origin) + if (origin.right != null) { + tryToRemergeLater.push(origin.right) + } + } + var distanceToOrigin = i = yield * Struct.Insert.getDistanceToOrigin.call(this, op) // most cases: 0 (starts from 0) + + // now we begin to insert op in the list of insertions.. + var o + var parent + var start + + // find o. o is the first conflicting operation + if (op.left != null) { + o = yield * this.getInsertionCleanEnd(op.left) + if (!Y.utils.compareIds(op.left, op.origin) && o.right != null) { + // only if not added previously + tryToRemergeLater.push(o.right) + } + o = (o.right == null) ? null : yield * this.getOperation(o.right) + } else { // left == null + parent = yield * this.getOperation(op.parent) + let startId = op.parentSub ? parent.map[op.parentSub] : parent.start + start = startId == null ? null : yield * this.getOperation(startId) + o = start + } + + // make sure to split op.right if necessary (also add to tryCombineWithLeft) + if (op.right != null) { + tryToRemergeLater.push(op.right) + yield * this.getInsertionCleanStart(op.right) + } + + // handle conflicts + while (true) { + if (o != null && !Y.utils.compareIds(o.id, op.right)) { + var oOriginDistance = yield * Struct.Insert.getDistanceToOrigin.call(this, o) + if (oOriginDistance === i) { + // case 1 + if (o.id[0] < op.id[0]) { + op.left = Y.utils.getLastId(o) + distanceToOrigin = i + 1 // just ignore o.content.length, doesn't make a difference } - } - return d - } - }, - /* - # $this has to find a unique position between origin and the next known character - # case 1: $origin equals $o.origin: the $creator parameter decides if left or right - # let $OL= [o1,o2,o3,o4], whereby $this is to be inserted between o1 and o4 - # o2,o3 and o4 origin is 1 (the position of o2) - # there is the case that $this.creator < o2.creator, but o3.creator < $this.creator - # then o2 knows o3. Since on another client $OL could be [o1,o3,o4] the problem is complex - # therefore $this would be always to the right of o3 - # case 2: $origin < $o.origin - # if current $this insert_position > $o origin: $this ins - # else $insert_position will not change - # (maybe we encounter case 1 later, then this will be to the right of $o) - # case 3: $origin > $o.origin - # $this insert_position is to the left of $o (forever!) - */ - execute: function * (op) { - var i // loop counter - - // during this function some ops may get split into two pieces (e.g. with getInsertionCleanEnd) - // We try to merge them later, if possible - var tryToRemergeLater = [] - - if (op.origin != null) { // TODO: !== instead of != - // we save in origin that op originates in it - // we need that later when we eventually garbage collect origin (see transaction) - var origin = yield * this.getInsertionCleanEnd(op.origin) - if (origin.originOf == null) { - origin.originOf = [] - } - origin.originOf.push(op.id) - yield * this.setOperation(origin) - if (origin.right != null) { - tryToRemergeLater.push(origin.right) - } - } - var distanceToOrigin = i = yield * Struct.Insert.getDistanceToOrigin.call(this, op) // most cases: 0 (starts from 0) - - // now we begin to insert op in the list of insertions.. - var o - var parent - var start - - // find o. o is the first conflicting operation - if (op.left != null) { - o = yield * this.getInsertionCleanEnd(op.left) - if (!Y.utils.compareIds(op.left, op.origin) && o.right != null) { - // only if not added previously - tryToRemergeLater.push(o.right) - } - o = (o.right == null) ? null : yield * this.getOperation(o.right) - } else { // left == null - parent = yield * this.getOperation(op.parent) - let startId = op.parentSub ? parent.map[op.parentSub] : parent.start - start = startId == null ? null : yield * this.getOperation(startId) - o = start - } - - // make sure to split op.right if necessary (also add to tryCombineWithLeft) - if (op.right != null) { - tryToRemergeLater.push(op.right) - yield * this.getInsertionCleanStart(op.right) - } - - // handle conflicts - while (true) { - if (o != null && !Y.utils.compareIds(o.id, op.right)) { - var oOriginDistance = yield * Struct.Insert.getDistanceToOrigin.call(this, o) - if (oOriginDistance === i) { - // case 1 - if (o.id[0] < op.id[0]) { - op.left = Y.utils.getLastId(o) - distanceToOrigin = i + 1 // just ignore o.content.length, doesn't make a difference - } - } else if (oOriginDistance < i) { - // case 2 - if (i - distanceToOrigin <= oOriginDistance) { - op.left = Y.utils.getLastId(o) - distanceToOrigin = i + 1 // just ignore o.content.length, doesn't make a difference - } - } else { - break - } - i++ - if (o.right != null) { - o = yield * this.getInsertion(o.right) - } else { - o = null + } else if (oOriginDistance < i) { + // case 2 + if (i - distanceToOrigin <= oOriginDistance) { + op.left = Y.utils.getLastId(o) + distanceToOrigin = i + 1 // just ignore o.content.length, doesn't make a difference } } else { break } - } - - // reconnect.. - var left = null - var right = null - if (parent == null) { - parent = yield * this.getOperation(op.parent) - } - - // reconnect left and set right of op - if (op.left != null) { - left = yield * this.getInsertion(op.left) - // link left - op.right = left.right - left.right = op.id - - yield * this.setOperation(left) - } else { - // set op.right from parent, if necessary - op.right = op.parentSub ? parent.map[op.parentSub] || null : parent.start - } - // reconnect right - if (op.right != null) { - // TODO: wanna connect right too? - right = yield * this.getOperation(op.right) - right.left = Y.utils.getLastId(op) - - // if right exists, and it is supposed to be gc'd. Remove it from the gc - if (right.gc != null) { - if (right.content != null && right.content.length > 1) { - right = yield * this.getInsertionCleanEnd(right.id) - } - this.store.removeFromGarbageCollector(right) - } - yield * this.setOperation(right) - } - - // update parents .map/start/end properties - if (op.parentSub != null) { - if (left == null) { - parent.map[op.parentSub] = op.id - yield * this.setOperation(parent) - } - // is a child of a map struct. - // Then also make sure that only the most left element is not deleted - // We do not call the type in this case (this is what the third parameter is for) - if (op.right != null) { - yield * this.deleteOperation(op.right, 1, true) - } - if (op.left != null) { - yield * this.deleteOperation(op.id, 1, true) + i++ + if (o.right != null) { + o = yield * this.getInsertion(o.right) + } else { + o = null } } else { - if (right == null || left == null) { - if (right == null) { - parent.end = Y.utils.getLastId(op) - } - if (left == null) { - parent.start = op.id - } - yield * this.setOperation(parent) - } - } - - // try to merge original op.left and op.origin - for (i = 0; i < tryToRemergeLater.length; i++) { - var m = yield * this.getOperation(tryToRemergeLater[i]) - yield * this.tryCombineWithLeft(m) + break } } - }, - List: { - /* - { + + // reconnect.. + var left = null + var right = null + if (parent == null) { + parent = yield * this.getOperation(op.parent) + } + + // reconnect left and set right of op + if (op.left != null) { + left = yield * this.getInsertion(op.left) + // link left + op.right = left.right + left.right = op.id + + yield * this.setOperation(left) + } else { + // set op.right from parent, if necessary + op.right = op.parentSub ? parent.map[op.parentSub] || null : parent.start + } + // reconnect right + if (op.right != null) { + // TODO: wanna connect right too? + right = yield * this.getOperation(op.right) + right.left = Y.utils.getLastId(op) + + // if right exists, and it is supposed to be gc'd. Remove it from the gc + if (right.gc != null) { + if (right.content != null && right.content.length > 1) { + right = yield * this.getInsertionCleanEnd(right.id) + } + this.store.removeFromGarbageCollector(right) + } + yield * this.setOperation(right) + } + + // update parents .map/start/end properties + if (op.parentSub != null) { + if (left == null) { + parent.map[op.parentSub] = op.id + yield * this.setOperation(parent) + } + // is a child of a map struct. + // Then also make sure that only the most left element is not deleted + // We do not call the type in this case (this is what the third parameter is for) + if (op.right != null) { + yield * this.deleteOperation(op.right, 1, true) + } + if (op.left != null) { + yield * this.deleteOperation(op.id, 1, true) + } + } else { + if (right == null || left == null) { + if (right == null) { + parent.end = Y.utils.getLastId(op) + } + if (left == null) { + parent.start = op.id + } + yield * this.setOperation(parent) + } + } + + // try to merge original op.left and op.origin + for (i = 0; i < tryToRemergeLater.length; i++) { + var m = yield * this.getOperation(tryToRemergeLater[i]) + yield * this.tryCombineWithLeft(m) + } + } + } + + /* + { + start: null, + end: null, + struct: "List", + type: "", + id: this.os.getNextOpId(1) + } + */ + Struct.List = { + create: function (id) { + return { start: null, end: null, - struct: "List", - type: "", - id: this.os.getNextOpId(1) - } - */ - create: function (id) { - return { - start: null, - end: null, - struct: 'List', - id: id - } - }, - encode: function (op) { - var e = { - struct: 'List', - id: op.id, - type: op.type - } - if (op.info != null) { - e.info = op.info - } - return e - }, - binaryEncode: function (encoder, op) { - encoder.writeUint8(CLIST) - encoder.writeOpID(op.id) - encoder.writeVarString(op.type) - let info = op.info != null ? JSON.stringify(op.info) : '' - encoder.writeVarString(info) - }, - binaryDecode: function (decoder) { - decoder.skip8() - let op = { - id: decoder.readOpID(), - type: decoder.readVarString(), - struct: 'List' - } - let info = decoder.readVarString() - if (info.length > 0) { - op.info = JSON.parse(info) - } - return op - }, - requiredOps: function () { - /* - var ids = [] - if (op.start != null) { - ids.push(op.start) - } - if (op.end != null){ - ids.push(op.end) - } - return ids - */ - return [] - }, - execute: function * (op) { - op.start = null - op.end = null - }, - ref: function * (op, pos) { - if (op.start == null) { - return null - } - var res = null - var o = yield * this.getOperation(op.start) - - while (true) { - if (!o.deleted) { - res = o - pos-- - } - if (pos >= 0 && o.right != null) { - o = yield * this.getOperation(o.right) - } else { - break - } - } - return res - }, - map: function * (o, f) { - o = o.start - var res = [] - while (o != null) { // TODO: change to != (at least some convention) - var operation = yield * this.getOperation(o) - if (!operation.deleted) { - res.push(f(operation)) - } - o = operation.right - } - return res + struct: 'List', + id: id } }, - Map: { + encode: function (op) { + var e = { + struct: 'List', + id: op.id, + type: op.type + } + return e + }, + binaryEncode: function (encoder, op) { + encoder.writeUint8(CLIST) + encoder.writeOpID(op.id) + encoder.writeVarString(op.type) + }, + binaryDecode: function (decoder) { + decoder.skip8() + let op = { + id: decoder.readOpID(), + type: decoder.readVarString(), + struct: 'List', + start: null, + end: null + } + return op + }, + requiredOps: function () { /* - { - map: {}, - struct: "Map", - type: "", - id: this.os.getNextOpId(1) - } + var ids = [] + if (op.start != null) { + ids.push(op.start) + } + if (op.end != null){ + ids.push(op.end) + } + return ids */ - create: function (id) { - return { - id: id, - map: {}, - struct: 'Map' + return [] + }, + execute: function * (op) { + op.start = null + op.end = null + }, + ref: function * (op, pos) { + if (op.start == null) { + return null + } + var res = null + var o = yield * this.getOperation(op.start) + + while (true) { + if (!o.deleted) { + res = o + pos-- } - }, - encode: function (op) { - var e = { - struct: 'Map', - type: op.type, - id: op.id, - map: {} // overwrite map!! + if (pos >= 0 && o.right != null) { + o = yield * this.getOperation(o.right) + } else { + break } - if (op.requires != null) { - e.requires = op.require - // TODO: !! - console.warn('requires is used! see same note above for List') + } + return res + }, + map: function * (o, f) { + o = o.start + var res = [] + while (o != null) { // TODO: change to != (at least some convention) + var operation = yield * this.getOperation(o) + if (!operation.deleted) { + res.push(f(operation)) } - if (op.info != null) { - e.info = op.info - } - return e - }, - binaryEncode: function (encoder, op) { - encoder.writeUint8(CMAP) - encoder.writeOpID(op.id) - encoder.writeVarString(op.type) - let info = op.info != null ? JSON.stringify(op.info) : '' - encoder.writeVarString(info) - }, - binaryDecode: function (decoder) { - decoder.skip8() - let op = { - id: decoder.readOpID(), - type: decoder.readVarString(), - struct: 'Map', - map: {} - } - let info = decoder.readVarString() - if (info.length > 0) { - op.info = JSON.parse(info) - } - return op - }, - requiredOps: function () { - return [] - }, - execute: function * () {}, - /* - Get a property by name - */ - get: function * (op, name) { - var oid = op.map[name] - if (oid != null) { - var res = yield * this.getOperation(oid) - if (res == null || res.deleted) { - return void 0 - } else if (res.opContent == null) { - return res.content[0] - } else { - return yield * this.getType(res.opContent) - } + o = operation.right + } + return res + } + } + + /* + { + map: {}, + struct: "Map", + type: "", + id: this.os.getNextOpId(1) + } + */ + Struct.Map = { + create: function (id) { + return { + id: id, + map: {}, + struct: 'Map' + } + }, + encode: function (op) { + var e = { + struct: 'Map', + type: op.type, + id: op.id, + map: {} // overwrite map!! + } + return e + }, + binaryEncode: function (encoder, op) { + encoder.writeUint8(CMAP) + encoder.writeOpID(op.id) + encoder.writeVarString(op.type) + }, + binaryDecode: function (decoder) { + decoder.skip8() + let op = { + id: decoder.readOpID(), + type: decoder.readVarString(), + struct: 'Map', + map: {} + } + return op + }, + requiredOps: function () { + return [] + }, + execute: function * (op) { + op.start = null + op.end = null + }, + /* + Get a property by name + */ + get: function * (op, name) { + var oid = op.map[name] + if (oid != null) { + var res = yield * this.getOperation(oid) + if (res == null || res.deleted) { + return void 0 + } else if (res.opContent == null) { + return res.content[0] + } else { + return yield * this.getType(res.opContent) } } } } - Y.Struct = Struct + + /* + { + map: {}, + start: null, + end: null, + struct: "Xml", + type: "", + id: this.os.getNextOpId(1) + } + */ + Struct.Xml = { + create: function (id, args) { + let nodeName = args != null ? args.nodeName : null + return { + id: id, + map: {}, + start: null, + end: null, + struct: 'Xml', + nodeName + } + }, + encode: function (op) { + var e = { + struct: 'Xml', + type: op.type, + id: op.id, + map: {}, + nodeName: op.nodeName + } + return e + }, + binaryEncode: function (encoder, op) { + encoder.writeUint8(CXML) + encoder.writeOpID(op.id) + encoder.writeVarString(op.type) + encoder.writeVarString(op.nodeName) + }, + binaryDecode: function (decoder) { + decoder.skip8() + let op = { + id: decoder.readOpID(), + type: decoder.readVarString(), + struct: 'Xml', + map: {}, + start: null, + end: null, + nodeName: decoder.readVarString() + } + return op + }, + requiredOps: function () { + return [] + }, + execute: function * () {}, + ref: Struct.List.ref, + map: Struct.List.map, + /* + Get a property by name + */ + get: Struct.Map.get + } } diff --git a/src/Transaction.js b/src/Transaction.js index 4e6932cd..bdc43cac 100644 --- a/src/Transaction.js +++ b/src/Transaction.js @@ -863,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 diff --git a/src/Utils.js b/src/Utils.js index a2ab394a..f5c0298c 100644 --- a/src/Utils.js +++ b/src/Utils.js @@ -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 + } } diff --git a/src/y.js b/src/y.js index 0dd86e43..2b4bc00e 100644 --- a/src/y.js +++ b/src/y.js @@ -176,23 +176,15 @@ 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) } }) diff --git a/test/encode-decode.js b/test/encode-decode.js index 7b9570ab..ce6d69b0 100644 --- a/test/encode-decode.js +++ b/test/encode-decode.js @@ -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 - }) }) diff --git a/test/y-xml.tests.js b/test/y-xml.tests.js new file mode 100644 index 00000000..1b06e8d9 --- /dev/null +++ b/test/y-xml.tests.js @@ -0,0 +1,288 @@ +import { wait, initArrays, compareUsers, Y, flushAll, garbageCollectUsers, applyRandomTests } from '../../yjs/tests-lib/helper.js' +import { test, proxyConsole } 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.Xml('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.Xml('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.Xml('A'), Y.Xml('B'), Y.Xml('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.Xml('A'), Y.Xml('B'), Y.Xml('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.Xml('A'), Y.Xml('B'), Y.Xml('C')]) + xml0.insert(0, [Y.Xml('X'), Y.Xml('Y'), Y.Xml('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) +}) diff --git a/tests-lib/helper.js b/tests-lib/helper.js index aef1c1a2..134061ce 100644 --- a/tests-lib/helper.js +++ b/tests-lib/helper.js @@ -3,14 +3,16 @@ 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' } @@ -47,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 @@ -73,6 +100,7 @@ export async function compareUsers (t, users) { } 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() @@ -133,6 +161,7 @@ export async function compareUsers (t, users) { 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') @@ -147,7 +176,7 @@ 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: 'Xml("div")' }, opts.share) var chance = opts.chance || new Chance(t.getSeed() * 1000000000) var conn = Object.assign({ room: 'debugging_' + t.name, generateUserId: false, testContext: t, chance }, connector) for (let i = 0; i < opts.users; i++) {