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++) {