diff --git a/examples/html-editor/index.js b/examples/html-editor/index.js
index 5b8d4195..4d8fe7ab 100644
--- a/examples/html-editor/index.js
+++ b/examples/html-editor/index.js
@@ -1,21 +1,17 @@
/* global Y */
// initialize a shared object. This function call returns a promise!
-Y({
- db: {
- name: 'memory'
- },
+let y = new Y({
connector: {
name: 'websockets-client',
url: 'http://127.0.0.1:1234',
room: 'html-editor-example6'
// maxBufferLength: 100
- },
- share: {
- xml: 'XmlFragment()' // y.share.xml is of type Y.Xml with tagname "p"
}
-}).then(function (y) {
- window.yXml = y
- // Bind children of XmlFragment to the document.body
- window.yXml.share.xml.bindToDom(document.body)
})
+window.yXml = y
+window.onload = function () {
+ console.log('start!')
+ // Bind children of XmlFragment to the document.body
+ y.get('xml', Y.XmlFragment).bindToDom(document.body)
+}
diff --git a/examples/yjs-dist.esm b/examples/yjs-dist.esm
index cecc1d9b..3c67e27e 100644
--- a/examples/yjs-dist.esm
+++ b/examples/yjs-dist.esm
@@ -1,12 +1,7 @@
-import Y from '../src/y.js'
-import yArray from '../../y-array/src/y-array.js'
-import yIndexedDB from '../../y-indexeddb/src/y-indexeddb.js'
-import yMap from '../../y-map/src/y-map.js'
-import yText from '../../y-text/src/y-text.js'
-import yXml from '../../y-xml/src/y-xml.js'
+import Y from '../src/Y.js'
import yWebsocketsClient from '../../y-websockets-client/src/y-websockets-client.js'
-Y.extend(yArray, yIndexedDB, yMap, yText, yXml, yWebsocketsClient)
+Y.extend(yWebsocketsClient)
export default Y
diff --git a/package-lock.json b/package-lock.json
index e6c4a05e..21cbe048 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1870,6 +1870,11 @@
"integrity": "sha1-ysNCuPqJAm7+c6Jg/p9rgE9J5H8=",
"dev": true
},
+ "fast-diff": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz",
+ "integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig=="
+ },
"fast-levenshtein": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
diff --git a/package.json b/package.json
index befd2203..e2f618ae 100644
--- a/package.json
+++ b/package.json
@@ -65,6 +65,7 @@
},
"dependencies": {
"debug": "^2.6.8",
+ "fast-diff": "^1.1.2",
"utf-8": "^1.0.0",
"utf8": "^2.1.2"
}
diff --git a/rollup.node.js b/rollup.node.js
index ec5d4375..1a795945 100644
--- a/rollup.node.js
+++ b/rollup.node.js
@@ -3,9 +3,9 @@ import commonjs from 'rollup-plugin-commonjs'
var pkg = require('./package.json')
export default {
- entry: 'src/Y.js',
+ entry: 'src/y-dist.cjs.js',
moduleName: 'Y',
- format: 'umd',
+ format: 'cjs',
plugins: [
nodeResolve({
main: true,
diff --git a/rollup.test.js b/rollup.test.js
index dca3e47d..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/y-array.tests.js',
+ entry: 'test/y-xml.tests.js',
moduleName: 'y-tests',
format: 'umd',
plugins: [
diff --git a/src/Connector.js b/src/Connector.js
index d7ac8f4b..5ee396c8 100644
--- a/src/Connector.js
+++ b/src/Connector.js
@@ -5,7 +5,7 @@ import { sendSyncStep1, readSyncStep1 } from './MessageHandler/syncStep1.js'
import { readSyncStep2 } from './MessageHandler/syncStep2.js'
import { readUpdate } from './MessageHandler/update.js'
-import { debug } from './Y.js'
+import debug from 'debug'
export default class AbstractConnector {
constructor (y, opts) {
@@ -251,9 +251,13 @@ export default class AbstractConnector {
// cannot wait for sync step 1 to finish, because we may wait for sync step 2 in sync step 1 (->lock)
readSyncStep1(decoder, encoder, this.y, senderConn, sender)
} else if (messageType === 'sync step 2' && senderConn.auth === 'write') {
- readSyncStep2(decoder, encoder, this.y, senderConn, sender)
+ this.y.transact(() => {
+ readSyncStep2(decoder, encoder, this.y, senderConn, sender)
+ })
} else if (messageType === 'update' && (skipAuth || senderConn.auth === 'write')) {
- readUpdate(decoder, encoder, this.y, senderConn, sender)
+ this.y.transact(() => {
+ readUpdate(decoder, encoder, this.y, senderConn, sender)
+ })
} else {
throw new Error('Unable to receive message')
}
diff --git a/src/MessageHandler/deleteSet.js b/src/MessageHandler/deleteSet.js
index e261639c..271234b8 100644
--- a/src/MessageHandler/deleteSet.js
+++ b/src/MessageHandler/deleteSet.js
@@ -1,4 +1,5 @@
import { deleteItemRange } from '../Struct/Delete.js'
+import ID from '../Util/ID.js'
export function stringifyDeleteSet (y, decoder, strBuilder) {
let dsLength = decoder.readUint32()
@@ -18,7 +19,7 @@ export function stringifyDeleteSet (y, decoder, strBuilder) {
export function writeDeleteSet (y, encoder) {
let currentUser = null
- let currentLength = 0
+ let currentLength
let lastLenPos
let numberOfUsers = 0
@@ -36,14 +37,17 @@ export function writeDeleteSet (y, encoder) {
if (currentUser !== null) { // happens on first iteration
encoder.setUint32(lastLenPos, currentLength)
}
+ currentUser = user
encoder.writeVarUint(user)
// pseudo-fill pos
lastLenPos = encoder.pos
encoder.writeUint32(0)
+ currentLength = 0
}
encoder.writeVarUint(clock)
encoder.writeVarUint(len)
encoder.writeUint8(gc ? 1 : 0)
+ currentLength++
})
if (currentUser !== null) { // happens on first iteration
encoder.setUint32(lastLenPos, currentLength)
@@ -56,62 +60,64 @@ export function readDeleteSet (y, decoder) {
for (let i = 0; i < dsLength; i++) {
let user = decoder.readVarUint()
let dv = []
- let dvLength = decoder.readVarUint()
+ let dvLength = decoder.readUint32()
for (let j = 0; j < dvLength; j++) {
let from = decoder.readVarUint()
let len = decoder.readVarUint()
let gc = decoder.readUint8() === 1
dv.push([from, len, gc])
}
- var pos = 0
- var d = dv[pos]
- y.ds.iterate([user, 0], [user, Number.MAX_VALUE], function (n) {
- // cases:
- // 1. d deletes something to the right of n
- // => go to next n (break)
- // 2. d deletes something to the left of n
- // => create deletions
- // => reset d accordingly
- // *)=> if d doesn't delete anything anymore, go to next d (continue)
- // 3. not 2) and d deletes something that also n deletes
- // => reset d so that it doesn't contain n's deletion
- // *)=> if d does not delete anything anymore, go to next d (continue)
- while (d != null) {
- var diff = 0 // describe the diff of length in 1) and 2)
- if (n.id[1] + n.len <= d[0]) {
- // 1)
- break
- } else if (d[0] < n.id[1]) {
- // 2)
- // delete maximum the len of d
- // else delete as much as possible
- diff = Math.min(n.id[1] - d[0], d[1])
- deleteItemRange(y, user, d[0], diff)
- // deletions.push([user, d[0], diff, d[2]])
- } else {
- // 3)
- diff = n.id[1] + n.len - d[0] // never null (see 1)
- if (d[2] && !n.gc) {
- // d marks as gc'd but n does not
- // then delete either way
- deleteItemRange(y, user, d[0], Math.min(diff, d[1]))
- // deletions.push([user, d[0], Math.min(diff, d[1]), d[2]])
+ if (dvLength > 0) {
+ let pos = 0
+ let d = dv[pos]
+ y.ds.iterate(new ID(user, 0), new ID(user, Number.MAX_VALUE), function (n) {
+ // cases:
+ // 1. d deletes something to the right of n
+ // => go to next n (break)
+ // 2. d deletes something to the left of n
+ // => create deletions
+ // => reset d accordingly
+ // *)=> if d doesn't delete anything anymore, go to next d (continue)
+ // 3. not 2) and d deletes something that also n deletes
+ // => reset d so that it doesn't contain n's deletion
+ // *)=> if d does not delete anything anymore, go to next d (continue)
+ while (d != null) {
+ var diff = 0 // describe the diff of length in 1) and 2)
+ if (n._id.clock + n.len <= d[0]) {
+ // 1)
+ break
+ } else if (d[0] < n._id.clock) {
+ // 2)
+ // delete maximum the len of d
+ // else delete as much as possible
+ diff = Math.min(n._id.clock - d[0], d[1])
+ deleteItemRange(y, user, d[0], diff)
+ // deletions.push([user, d[0], diff, d[2]])
+ } else {
+ // 3)
+ diff = n._id.clock + n.len - d[0] // never null (see 1)
+ if (d[2] && !n.gc) {
+ // d marks as gc'd but n does not
+ // then delete either way
+ deleteItemRange(y, user, d[0], Math.min(diff, d[1]))
+ // deletions.push([user, d[0], Math.min(diff, d[1]), d[2]])
+ }
+ }
+ if (d[1] <= diff) {
+ // d doesn't delete anything anymore
+ d = dv[++pos]
+ } else {
+ d[0] = d[0] + diff // reset pos
+ d[1] = d[1] - diff // reset length
}
}
- if (d[1] <= diff) {
- // d doesn't delete anything anymore
- d = dv[++pos]
- } else {
- d[0] = d[0] + diff // reset pos
- d[1] = d[1] - diff // reset length
- }
+ })
+ // for the rest.. just apply it
+ for (; pos < dv.length; pos++) {
+ d = dv[pos]
+ deleteItemRange(y, user, d[0], d[1])
+ // deletions.push([user, d[0], d[1], d[2]])
}
- })
- // for the rest.. just apply it
- for (; pos < dv.length; pos++) {
- d = dv[pos]
- deleteItemRange(y, user, d[0], d[1])
- // deletions.push([user, d[0], d[1], d[2]])
}
}
}
diff --git a/src/MessageHandler/integrateRemoteStructs.js b/src/MessageHandler/integrateRemoteStructs.js
index 05bd393a..8bc2ba14 100644
--- a/src/MessageHandler/integrateRemoteStructs.js
+++ b/src/MessageHandler/integrateRemoteStructs.js
@@ -1,5 +1,6 @@
import { getStruct } from '../Util/structReferences.js'
import BinaryDecoder from '../Binary/Decoder.js'
+import Delete from '../Struct/Delete.js'
class MissingEntry {
constructor (decoder, missing, struct) {
@@ -16,24 +17,26 @@ class MissingEntry {
*/
function _integrateRemoteStructHelper (y, struct) {
struct._integrate(y)
- let msu = y._missingStructs.get(struct._id.user)
- if (msu != null) {
- let len = struct._length
- for (let i = 0; i < len; i++) {
- if (msu.has(struct._id.clock + i)) {
- let msuc = msu.get(struct._id.clock + i)
- msuc.forEach(missingDef => {
- missingDef.missing--
- if (missingDef.missing === 0) {
- let missing = missingDef.struct._fromBinary(y, missingDef.decoder)
- if (missing.length > 0) {
- console.error('Missing should be empty!')
- } else {
- y._readyToIntegrate.push(missingDef.struct)
+ if (!(struct instanceof Delete)) {
+ let msu = y._missingStructs.get(struct._id.user)
+ if (msu != null) {
+ let len = struct._length
+ for (let i = 0; i < len; i++) {
+ if (msu.has(struct._id.clock + i)) {
+ let msuc = msu.get(struct._id.clock + i)
+ msuc.forEach(missingDef => {
+ missingDef.missing--
+ if (missingDef.missing === 0) {
+ let missing = missingDef.struct._fromBinary(y, missingDef.decoder)
+ if (missing.length > 0) {
+ console.error('Missing should be empty!')
+ } else {
+ y._readyToIntegrate.push(missingDef.struct)
+ }
}
- }
- })
- msu.delete(struct._id.clock)
+ })
+ msu.delete(struct._id.clock)
+ }
}
}
}
diff --git a/src/MessageHandler/messageToString.js b/src/MessageHandler/messageToString.js
index 405feeb9..ca8dadf4 100644
--- a/src/MessageHandler/messageToString.js
+++ b/src/MessageHandler/messageToString.js
@@ -3,18 +3,18 @@ import { stringifyUpdate } from './update.js'
import { stringifySyncStep1 } from './syncStep1.js'
import { stringifySyncStep2 } from './syncStep2.js'
-export function messageToString (buffer) {
+export function messageToString (y, buffer) {
let decoder = new BinaryDecoder(buffer)
decoder.readVarString() // read roomname
let type = decoder.readVarString()
let strBuilder = []
strBuilder.push('\n === ' + type + ' ===\n')
if (type === 'update') {
- stringifyUpdate(decoder, strBuilder)
+ stringifyUpdate(y, decoder, strBuilder)
} else if (type === 'sync step 1') {
- stringifySyncStep1(decoder, strBuilder)
+ stringifySyncStep1(y, decoder, strBuilder)
} else if (type === 'sync step 2') {
- stringifySyncStep2(decoder, strBuilder)
+ stringifySyncStep2(y, decoder, strBuilder)
} else {
strBuilder.push('-- Unknown message type - probably an encoding issue!!!')
}
diff --git a/src/MessageHandler/syncStep1.js b/src/MessageHandler/syncStep1.js
index 632a61ff..7908eeea 100644
--- a/src/MessageHandler/syncStep1.js
+++ b/src/MessageHandler/syncStep1.js
@@ -2,8 +2,9 @@ import BinaryEncoder from '../Binary/Encoder.js'
import { readStateSet, writeStateSet } from './stateSet.js'
import { writeDeleteSet } from './deleteSet.js'
import ID from '../Util/ID.js'
+import { RootFakeUserID } from '../Util/RootID.js'
-export function stringifySyncStep1 (decoder, strBuilder) {
+export function stringifySyncStep1 (y, decoder, strBuilder) {
let auth = decoder.readVarString()
let protocolVersion = decoder.readVarUint()
strBuilder.push(`
@@ -31,10 +32,13 @@ export function sendSyncStep1 (connector, syncUser) {
}
export default function writeStructs (encoder, decoder, y, ss) {
- for (let [user, clock] of ss) {
- y.os.iterate(new ID(user, clock), null, function (struct) {
- struct._toBinary(encoder)
- })
+ for (let user of y.ss.state.keys()) {
+ let clock = ss.get(user) || 0
+ if (user !== RootFakeUserID) {
+ y.os.iterate(new ID(user, clock), new ID(user, Number.MAX_VALUE), function (struct) {
+ struct._toBinary(encoder)
+ })
+ }
}
}
diff --git a/src/MessageHandler/syncStep2.js b/src/MessageHandler/syncStep2.js
index 13c1642e..387e54d1 100644
--- a/src/MessageHandler/syncStep2.js
+++ b/src/MessageHandler/syncStep2.js
@@ -2,10 +2,10 @@ import { integrateRemoteStructs } from './integrateRemoteStructs.js'
import { stringifyUpdate } from './update.js'
import { readDeleteSet } from './deleteSet.js'
-export function stringifySyncStep2 (decoder, strBuilder) {
+export function stringifySyncStep2 (y, decoder, strBuilder) {
strBuilder.push(' - auth: ' + decoder.readVarString() + '\n')
strBuilder.push(' == OS: \n')
- stringifyUpdate(decoder, strBuilder)
+ stringifyUpdate(y, decoder, strBuilder)
// write DS to string
strBuilder.push(' == DS: \n')
let len = decoder.readUint32()
diff --git a/src/MessageHandler/update.js b/src/MessageHandler/update.js
index 85a64503..198c85ed 100644
--- a/src/MessageHandler/update.js
+++ b/src/MessageHandler/update.js
@@ -1,12 +1,12 @@
import { getStruct } from '../Util/structReferences.js'
-export function stringifyUpdate (decoder, strBuilder) {
+export function stringifyUpdate (y, decoder, strBuilder) {
while (decoder.length !== decoder.pos) {
let reference = decoder.readVarUint()
let Constr = getStruct(reference)
let struct = new Constr()
- let missing = struct._fromBinary(decoder)
+ let missing = struct._fromBinary(y, decoder)
let logMessage = struct._logString()
if (missing.length > 0) {
logMessage += missing.map(m => m._logString()).join(', ')
diff --git a/src/Store/DeleteStore.js b/src/Store/DeleteStore.js
index 873126be..68aa01a2 100644
--- a/src/Store/DeleteStore.js
+++ b/src/Store/DeleteStore.js
@@ -3,25 +3,26 @@ import ID from '../Util/ID.js'
class DSNode {
constructor (id, len, gc) {
- this.id = id
+ this._id = id
this.len = len
this.gc = gc
}
clone () {
- return new DSNode(this.id, this.len, this.gc)
+ return new DSNode(this._id, this.len, this.gc)
}
}
export default class DeleteStore extends Tree {
isDeleted (id) {
- var n = this.ds.findWithUpperBound(id)
- return n != null && n.id[0] === id[0] && id[1] < n.id[1] + n.len
+ var n = this.findWithUpperBound(id)
+ return n !== null && n._id.user === id.user && id.clock < n._id.clock + n.len
}
+ // TODO: put this in function (and all other methods)
applyMissingDeletesOnStruct (struct) {
const strID = struct._id
// find most right delete
- let n = this.findWithUpperBound(new ID(strID.user, strID.clock + struct.length - 1))
- if (n === null || n.id.user !== strID.user || n.id.clock + n.length <= strID.clock) {
+ let n = this.findWithUpperBound(new ID(strID.user, strID.clock + struct._length - 1))
+ if (n === null || n._id.user !== strID.user || n._id.clock + n.len <= strID.clock) {
// struct is not deleted
return null
}
@@ -37,22 +38,22 @@ export default class DeleteStore extends Tree {
throw new Error('length must be defined')
}
var n = this.findWithUpperBound(id)
- if (n != null && n.id.user === id.user) {
- if (n.id.clock <= id.clock && id.clock <= n.id.clock + n.len) {
+ if (n != null && n._id.user === id.user) {
+ if (n._id.clock <= id.clock && id.clock <= n._id.clock + n.len) {
// id is in n's range
- var diff = id.clock + length - (n.id.clock + n.len) // overlapping right
+ var diff = id.clock + length - (n._id.clock + n.len) // overlapping right
if (diff > 0) {
// id+length overlaps n
if (!n.gc) {
n.len += diff
} else {
- diff = n.id.clock + n.len - id.clock // overlapping left (id till n.end)
+ diff = n._id.clock + n.len - id.clock // overlapping left (id till n.end)
if (diff < length) {
// a partial deletion
let nId = id.clone()
nId.clock += diff
n = new DSNode(nId, length - diff, false)
- this.ds.put(n)
+ this.put(n)
} else {
// already gc'd
throw new Error(
@@ -67,21 +68,21 @@ export default class DeleteStore extends Tree {
} else {
// cannot extend left (there is no left!)
n = new DSNode(id, length, false)
- this.ds.put(n) // TODO: you double-put !!
+ this.put(n) // TODO: you double-put !!
}
} else {
// cannot extend left
n = new DSNode(id, length, false)
- this.ds.put(n)
+ this.put(n)
}
// can extend right?
- var next = this.ds.findNext(n.id)
+ var next = this.findNext(n._id)
if (
next != null &&
- n.id.user === next.id.user &&
- n.id.clock + n.len >= next.id.clock
+ n._id.user === next._id.user &&
+ n._id.clock + n.len >= next._id.clock
) {
- diff = n.id.clock + n.len - next.id.clock // from next.start to n.end
+ diff = n._id.clock + n.len - next._id.clock // from next.start to n.end
while (diff >= 0) {
// n overlaps with next
if (next.gc) {
@@ -92,7 +93,7 @@ export default class DeleteStore extends Tree {
diff = diff - next.len // missing range after next
if (diff > 0) {
this.put(n) // unneccessary? TODO!
- this.markDeleted(new ID(next.id.user, next.id.clock + next.len), diff)
+ this.markDeleted(new ID(next._id.user, next._id.clock + next.len), diff)
}
}
break
@@ -101,19 +102,19 @@ export default class DeleteStore extends Tree {
if (diff > next.len) {
// n is even longer than next
// get next.next, and try to extend it
- var _next = this.findNext(next.id)
- this.delete(next.id)
- if (_next == null || n.id.user !== _next.id.user) {
+ var _next = this.findNext(next._id)
+ this.delete(next._id)
+ if (_next == null || n._id.user !== _next._id.user) {
break
} else {
next = _next
- diff = n.id.clock + n.len - next.id.clock // from next.start to n.end
+ diff = n._id.clock + n.len - next._id.clock // from next.start to n.end
// continue!
}
} else {
// n just partially overlaps with next. extend n, delete next, and break this loop
n.len += next.len - diff
- this.delete(next.id)
+ this.delete(next._id)
break
}
}
diff --git a/src/Store/OperationStore.js b/src/Store/OperationStore.js
index f6be01f8..4587d664 100644
--- a/src/Store/OperationStore.js
+++ b/src/Store/OperationStore.js
@@ -1,5 +1,5 @@
import Tree from '../Util/Tree.js'
-import RootID from '../Util/ID.js'
+import RootID from '../Util/RootID.js'
import { getStruct } from '../Util/structReferences.js'
export default class OperationStore extends Tree {
@@ -10,10 +10,12 @@ export default class OperationStore extends Tree {
get (id) {
let struct = this.find(id)
if (struct === null && id instanceof RootID) {
- let Constr = getStruct(id.type)
+ const Constr = getStruct(id.type)
+ const y = this.y
struct = new Constr()
struct._id = id
- struct._parent = this.y
+ struct._parent = y
+ struct._integrate(y)
this.put(struct)
}
return struct
diff --git a/src/Store/StateStore.js b/src/Store/StateStore.js
index fe726f22..13a91f32 100644
--- a/src/Store/StateStore.js
+++ b/src/Store/StateStore.js
@@ -4,12 +4,12 @@ export default class StateStore {
constructor (y) {
this.y = y
this.state = new Map()
- this.currentClock = 0
}
getNextID (len) {
- let id = new ID(this.y.userID, this.currentClock)
- this.currentClock += len
- return id
+ const user = this.y.userID
+ const state = this.getState(user)
+ this.setState(user, state + len)
+ return new ID(user, state)
}
updateRemoteState (struct) {
let user = struct._id.user
@@ -27,4 +27,8 @@ export default class StateStore {
}
return state
}
+ setState (user, state) {
+ // TODO: modify missingi structs here
+ this.state.set(user, state)
+ }
}
diff --git a/src/Struct/Delete.js b/src/Struct/Delete.js
index e195b1c2..ecfc717b 100644
--- a/src/Struct/Delete.js
+++ b/src/Struct/Delete.js
@@ -1,9 +1,35 @@
import { getReference } from '../Util/structReferences.js'
+import ID from '../Util/ID.js'
+/**
+ * Delete all items in an ID-range
+ * TODO: implement getItemCleanStartNode for better performance (only one lookup)
+ */
export function deleteItemRange (y, user, clock, range) {
- let items = y.os.getItems(this._target, this._length)
- for (let i = items.length - 1; i >= 0; i--) {
- items[i]._delete(y, false)
+ const createDelete = y.connector._forwardAppliedStructs
+ let item = y.os.getItemCleanStart(new ID(user, clock))
+ if (item !== null) {
+ if (!item._deleted) {
+ item._splitAt(y, range)
+ item._delete(y, createDelete)
+ }
+ let itemLen = item._length
+ range -= itemLen
+ clock += itemLen
+ if (range > 0) {
+ let node = y.os.findNode(new ID(user, clock))
+ while (node !== null && range > 0 && node.val._id.equals(new ID(user, clock))) {
+ const nodeVal = node.val
+ if (!nodeVal._deleted) {
+ nodeVal._splitAt(y, range)
+ nodeVal._delete(y, createDelete)
+ }
+ const nodeLen = nodeVal._length
+ range -= nodeLen
+ clock += nodeLen
+ node = node.next()
+ }
+ }
}
}
@@ -18,6 +44,7 @@ export default class Delete {
_fromBinary (y, decoder) {
this._targetID = decoder.readID()
this._length = decoder.readVarUint()
+ return []
}
_toBinary (encoder) {
encoder.writeUint8(getReference(this.constructor))
diff --git a/src/Struct/Item.js b/src/Struct/Item.js
index 4f7413e4..69da21d2 100644
--- a/src/Struct/Item.js
+++ b/src/Struct/Item.js
@@ -34,6 +34,9 @@ export default class Item {
this._parentSub = null
this._deleted = false
}
+ get _lastId () {
+ return new ID(this._id.user, this._id.clock + this._length - 1)
+ }
get _length () {
return 1
}
@@ -61,6 +64,17 @@ export default class Item {
del._length = this._length
del._integrate(y, true)
}
+ const parent = this._parent
+ if (parent !== y && !parent._deleted) {
+ y._transactionChangedTypes.set(parent, this._parentSub)
+ }
+ }
+ /**
+ * This is called right before this struct receives any children.
+ * It can be overwritten to apply pending changes before applying remote changes
+ */
+ _beforeChange () {
+ // nop
}
/*
* - Integrate the struct so that other types/structs can see it
@@ -68,12 +82,26 @@ export default class Item {
* - Check if this is struct deleted
*/
_integrate (y) {
+ const parent = this._parent
const selfID = this._id
+ const userState = selfID === null ? 0 : y.ss.getState(selfID.user)
if (selfID === null) {
this._id = y.ss.getNextID(this._length)
- } else if (selfID.clock < y.ss.getState(selfID.user)) {
+ } else if (selfID.user === RootFakeUserID) {
+ // nop
+ } else if (selfID.clock < userState) {
// already applied..
return []
+ } else if (selfID.clock === userState) {
+ y.ss.setState(selfID.user, userState + this._length)
+ } else {
+ // missing content from user
+ throw new Error('Can not apply yet!')
+ }
+ if (!parent._deleted && !y._transactionChangedTypes.has(parent) && !y._transactionNewTypes.has(parent)) {
+ // this is the first time parent is updated
+ // or this types is new
+ this._parent._beforeChange()
}
/*
# $this has to find a unique position between origin and the next known character
@@ -96,7 +124,7 @@ export default class Item {
if (this._left !== null) {
o = this._left._right
} else if (this._parentSub !== null) {
- o = this._parent._map.get(this._parentSub)
+ o = this._parent._map.get(this._parentSub) || null
} else {
o = this._parent._start
}
@@ -124,14 +152,36 @@ export default class Item {
}
o = o._right
}
+ // reconnect left/right + update parent map/start if necessary
+ const parentSub = this._parentSub
if (this._left === null) {
- if (this._parentSub !== null) {
- this._parent._map.set(this._parentSub, this)
+ let right
+ if (parentSub !== null) {
+ const pmap = parent._map
+ right = pmap.get(parentSub) || null
+ pmap.set(parentSub, this)
} else {
- this._parent._start = this
+ right = parent._start
+ parent._start = this
+ }
+ this._right = right
+ if (right !== null) {
+ right._left = this
+ }
+ } else {
+ const left = this._left
+ const right = left._right
+ this._right = right
+ left._right = this
+ if (right !== null) {
+ right._left = this
}
}
y.os.put(this)
+ if (parent !== y && !parent._deleted) {
+ y._transactionChangedTypes.set(parent, parentSub)
+ }
+
if (this._id.user !== RootFakeUserID) {
if (y.connector._forwardAppliedStructs || this._id.user === y.userID) {
y.connector.broadcastStruct(this)
@@ -160,10 +210,10 @@ export default class Item {
encoder.writeUint8(info)
encoder.writeID(this._id)
if (info & 0b1) {
- encoder.writeID(this._origin._id)
+ encoder.writeID(this._origin._lastId)
}
if (info & 0b10) {
- encoder.writeID(this._left._id)
+ encoder.writeID(this._left._lastId)
}
if (info & 0b100) {
encoder.writeID(this._right_origin._id)
@@ -179,7 +229,8 @@ export default class Item {
_fromBinary (y, decoder) {
let missing = []
const info = decoder.readUint8()
- this._id = decoder.readID()
+ const id = decoder.readID()
+ this._id = id
// read origin
if (info & 0b1) {
// origin != null
@@ -214,9 +265,9 @@ export default class Item {
// right != null
const rightID = decoder.readID()
if (this._right_origin === null) {
- const right = y.os.getCleanStart(rightID)
+ const right = y.os.getItemCleanStart(rightID)
if (right === null) {
- missing.push(right)
+ missing.push(rightID)
} else {
this._right = right
this._right_origin = right
@@ -230,7 +281,7 @@ export default class Item {
if (this._parent === null) {
const parent = y.os.get(parentID)
if (parent === null) {
- missing.push(parent)
+ missing.push(parentID)
} else {
this._parent = parent
}
@@ -239,11 +290,15 @@ export default class Item {
if (this._origin !== null) {
this._parent = this._origin._parent
} else if (this._right_origin !== null) {
- this._parent = this._origin._parent
+ this._parent = this._right_origin._parent
}
}
if (info & 0b1000) {
- this._parentSub = decoder.readVarString()
+ // TODO: maybe put this in read parent condition (you can also read parentsub from left/right)
+ this._parentSub = JSON.parse(decoder.readVarString())
+ }
+ if (y.ss.getState(id.user) < id.clock) {
+ missing.push(new ID(id.user, id.clock - 1))
}
return missing
}
diff --git a/src/Struct/ItemJSON.js b/src/Struct/ItemJSON.js
index 25f00cf6..d62403de 100644
--- a/src/Struct/ItemJSON.js
+++ b/src/Struct/ItemJSON.js
@@ -13,7 +13,8 @@ export default class ItemJSON extends Item {
let len = decoder.readVarUint()
this._content = new Array(len)
for (let i = 0; i < len; i++) {
- this._content[i] = JSON.parse(decoder.readVarString())
+ const ctnt = decoder.readVarString()
+ this._content[i] = JSON.parse(ctnt)
}
return missing
}
diff --git a/src/Struct/Type.js b/src/Struct/Type.js
index e595d0ae..21ffd3f4 100644
--- a/src/Struct/Type.js
+++ b/src/Struct/Type.js
@@ -1,4 +1,18 @@
import Item from './Item.js'
+import EventHandler from '../Util/EventHandler.js'
+
+// restructure children as if they were inserted one after another
+function integrateChildren (y, start) {
+ let right
+ do {
+ right = start._right
+ start._right = null
+ start._right_origin = null
+ start._origin = start._left
+ start._integrate(y)
+ start = right
+ } while (right !== null)
+}
export default class Type extends Item {
constructor () {
@@ -6,24 +20,46 @@ export default class Type extends Item {
this._map = new Map()
this._start = null
this._y = null
+ this._eventHandler = new EventHandler()
+ }
+ observe (f) {
+ this._eventHandler.addEventListener(f)
+ }
+ unobserve (f) {
+ this._eventHandler.removeEventListener(f)
}
_integrate (y) {
+ y._transactionNewTypes.add(this)
super._integrate(y)
this._y = y
+ // when integrating children we must make sure to
+ // integrate start
+ const start = this._start
+ if (start !== null) {
+ this._start = null
+ integrateChildren(y, start)
+ }
+ // integrate map children
+ const map = this._map
+ for (let [key, t] of map) {
+ map.delete(key)
+ integrateChildren(y, t)
+ }
}
- _delete (y) {
- super._delete(y)
+ _delete (y, createDelete) {
+ super._delete(y, createDelete)
+ y._transactionChangedTypes.delete(this)
// delete map types
for (let value of this._map.values()) {
if (value instanceof Item && !value._deleted) {
- value._delete()
+ value._delete(y, false)
}
}
// delete array types
let t = this._start
while (t !== null) {
if (!t._deleted) {
- t._delete()
+ t._delete(y, false)
}
t = t._right
}
diff --git a/src/Type/YArray.js b/src/Type/YArray.js
index 3c24ba25..2649c3a8 100644
--- a/src/Type/YArray.js
+++ b/src/Type/YArray.js
@@ -2,6 +2,16 @@ import Type from '../Struct/Type.js'
import ItemJSON from '../Struct/ItemJSON.js'
export default class YArray extends Type {
+ _callObserver () {
+ this._eventHandler.callEventListeners({})
+ }
+ get (i) {
+ // TODO: This can be improved!
+ return this.toArray()[i]
+ }
+ toArray () {
+ return this.map(c => c)
+ }
toJSON () {
return this.map(c => {
if (c instanceof Type) {
@@ -11,6 +21,7 @@ export default class YArray extends Type {
return c.toString()
}
}
+ return c
})
}
map (f) {
@@ -25,11 +36,15 @@ export default class YArray extends Type {
let n = this._start
while (n !== null) {
if (!n._deleted) {
- const content = n._content
- const contentLen = content.length
- for (let i = 0; i < contentLen; i++) {
- pos++
- f(content[i], pos, this)
+ if (n instanceof Type) {
+ f(n, pos++, this)
+ } else {
+ const content = n._content
+ const contentLen = content.length
+ for (let i = 0; i < contentLen; i++) {
+ pos++
+ f(content[i], pos, this)
+ }
}
}
n = n._right
@@ -42,14 +57,14 @@ export default class YArray extends Type {
if (!n._deleted) {
length += n._length
}
- n = n._next
+ n = n._right
}
return length
}
[Symbol.iterator] () {
return {
next: function () {
- while (this._item !== null && (this._item._deleted || this._item._content.length <= this._itemElement)) {
+ while (this._item !== null && (this._item._deleted || this._item._length <= this._itemElement)) {
// item is deleted or itemElement does not exist (is deleted)
this._item = this._item._right
this._itemElement = 0
@@ -58,11 +73,16 @@ export default class YArray extends Type {
return {
done: true
}
+ }
+ let content
+ if (this._item instanceof Type) {
+ content = this._item
} else {
- return {
- value: [this._count, this._item._content[this._itemElement++]],
- done: false
- }
+ content = this._item._content[this._itemElement++]
+ }
+ return {
+ value: [this._count, content],
+ done: false
}
},
_item: this._start,
@@ -71,68 +91,99 @@ export default class YArray extends Type {
}
}
delete (pos, length = 1) {
- let item = this._start
- let count = 0
- while (item !== null && length > 0) {
- if (count < pos && pos < count + item._length) {
- const diffDel = pos - count
- item = item
- ._splitAt(this._y, diffDel)
- ._splitAt(this._y, length)
- length -= item._length
- item._delete(this._y)
+ this._y.transact(() => {
+ let item = this._start
+ let count = 0
+ while (item !== null && length > 0) {
+ if (count <= pos && pos < count + item._length) {
+ const diffDel = pos - count
+ item = item._splitAt(this._y, diffDel)
+ item._splitAt(this._y, length)
+ length -= item._length
+ item._delete(this._y)
+ }
+ if (!item._deleted) {
+ count += item._length
+ }
+ item = item._right
}
- if (!item._deleted) {
- count += item._length
+ if (length > 0) {
+ throw new Error('Delete exceeds the range of the YArray')
+ }
+ })
+ }
+ insertAfter (left, content) {
+ const apply = () => {
+ let right
+ if (left === null) {
+ right = this._start
+ } else {
+ right = left._right
+ }
+ let prevJsonIns = null
+ for (let i = 0; i < content.length; i++) {
+ let c = content[i]
+ if (c instanceof Type) {
+ if (prevJsonIns !== null) {
+ if (this._y !== null) {
+ prevJsonIns._integrate(this._y)
+ }
+ left = prevJsonIns
+ prevJsonIns = null
+ }
+ c._origin = left
+ c._left = left
+ c._right = right
+ c._right_origin = right
+ c._parent = this
+ if (this._y !== null) {
+ c._integrate(this._y)
+ } else if (left === null) {
+ this._start = c
+ }
+ left = c
+ } else {
+ if (prevJsonIns === null) {
+ prevJsonIns = new ItemJSON()
+ prevJsonIns._origin = left
+ prevJsonIns._left = left
+ prevJsonIns._right = right
+ prevJsonIns._right_origin = right
+ prevJsonIns._parent = this
+ prevJsonIns._content = []
+ }
+ prevJsonIns._content.push(c)
+ }
+ }
+ if (prevJsonIns !== null && this._y !== null) {
+ prevJsonIns._integrate(this._y)
}
- item = item._right
}
- if (length > 0) {
- throw new Error('Delete exceeds the range of the YArray')
+ if (this._y !== null) {
+ this._y.transact(apply)
+ } else {
+ apply()
}
+ return content
}
insert (pos, content) {
- let left = this._start
- let right = null
+ let left = null
+ let right = this._start
let count = 0
- while (left !== null) {
- if (count <= pos && pos < count + left._content.length) {
- right = left._splitAt(this.y, pos - count)
+ while (right !== null) {
+ if (count <= pos && pos < count + right._length) {
+ right = right._splitAt(this._y, pos - count)
+ left = right._left
break
}
- count += left._length
- left = left.right
+ count += right._length
+ left = right
+ right = right._right
}
if (pos > count) {
throw new Error('Position exceeds array range!')
}
- let prevJsonIns = null
- for (let i = 0; i < content.length; i++) {
- let c = content[i]
- if (c instanceof Type) {
- if (prevJsonIns === null) {
- prevJsonIns._integrate(this._y)
- prevJsonIns = null
- }
- c._left = left
- c._origin = left
- c._right = right
- c._parent = this
- } else {
- if (prevJsonIns === null) {
- prevJsonIns = new ItemJSON()
- prevJsonIns._origin = left
- prevJsonIns._left = left
- prevJsonIns._right = right
- prevJsonIns._parent = this
- prevJsonIns._content = []
- }
- prevJsonIns._content.push(c)
- }
- }
- if (prevJsonIns !== null) {
- prevJsonIns._integrate(this._y)
- }
+ this.insertAfter(left, content)
}
_logString () {
let s = super._logString()
diff --git a/src/Type/YMap.js b/src/Type/YMap.js
index 58189352..3ed44973 100644
--- a/src/Type/YMap.js
+++ b/src/Type/YMap.js
@@ -3,6 +3,11 @@ import Item from '../Struct/Item.js'
import ItemJSON from '../Struct/ItemJSON.js'
export default class YMap extends Type {
+ _callObserver (parentSub) {
+ this._eventHandler.callEventListeners({
+ name: parentSub
+ })
+ }
toJSON () {
const map = {}
for (let [key, item] of this._map) {
@@ -22,22 +27,40 @@ export default class YMap extends Type {
}
return map
}
+ delete (key) {
+ this._y.transact(() => {
+ let c = this._map.get(key)
+ if (c !== undefined) {
+ c._delete(this._y)
+ }
+ })
+ }
set (key, value) {
- let old = this._map.get(key)
- let v
- if (value instanceof Item) {
- v = value
- } else {
- let v = new ItemJSON()
- v._content = JSON.stringify(value)
- }
- v._right = old
- v._parent = this
- v._parentSub = key
- v._integrate()
+ this._y.transact(() => {
+ const old = this._map.get(key) || null
+ if (old !== null) {
+ old._delete(this._y)
+ }
+ let v
+ if (value instanceof Item) {
+ v = value
+ } else {
+ v = new ItemJSON()
+ v._content = [value]
+ }
+ v._right = old
+ v._right_origin = old
+ v._parent = this
+ v._parentSub = key
+ v._integrate(this._y)
+ })
+ return value
}
get (key) {
let v = this._map.get(key)
+ if (v === undefined || v._deleted) {
+ return undefined
+ }
if (v instanceof Type) {
return v
} else {
diff --git a/src/Type/YText.js b/src/Type/YText.js
index ce3744ce..ce88adb5 100644
--- a/src/Type/YText.js
+++ b/src/Type/YText.js
@@ -1,4 +1,53 @@
+import ItemString from '../Struct/ItemString.js'
import YArray from './YArray.js'
export default class YText extends YArray {
+ constructor (string) {
+ super()
+ if (typeof string === 'string') {
+ const start = new ItemString()
+ start._parent = this
+ start._content = string
+ this._start = start
+ }
+ }
+ toString () {
+ const strBuilder = []
+ let n = this._start
+ while (n !== null) {
+ if (!n._deleted) {
+ strBuilder.push(n._content)
+ }
+ n = n._right
+ }
+ return strBuilder.join('')
+ }
+ insert (pos, text) {
+ this._y.transact(() => {
+ let left = null
+ let right = this._start
+ let count = 0
+ while (right !== null) {
+ if (count <= pos && pos < count + right._length) {
+ right = right._splitAt(this._y, pos - count)
+ left = right._left
+ break
+ }
+ count += right._length
+ left = right
+ right = right._right
+ }
+ if (pos > count) {
+ throw new Error('Position exceeds array range!')
+ }
+ let item = new ItemString()
+ item._origin = left
+ item._left = left
+ item._right = right
+ item._right_origin = right
+ item._parent = this
+ item._content = text
+ item._integrate(this._y)
+ })
+ }
}
diff --git a/src/Type/YXml.js b/src/Type/YXml.js
deleted file mode 100644
index 4e0bd83e..00000000
--- a/src/Type/YXml.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import YArray from './YArray.js'
-
-export default class YXml extends YArray {
- setDomFilter () {
- // TODO
- }
- toString () {
- return ''
- }
-}
diff --git a/src/Type/y-xml/YXmlElement.js b/src/Type/y-xml/YXmlElement.js
new file mode 100644
index 00000000..118756ab
--- /dev/null
+++ b/src/Type/y-xml/YXmlElement.js
@@ -0,0 +1,117 @@
+/* global MutationObserver */
+
+// import diff from 'fast-diff'
+import { defaultDomFilter } from './utils.js'
+
+import YMap from '../YMap.js'
+import YXmlFragment from './YXmlFragment.js'
+
+export default class YXmlElement extends YXmlFragment {
+ constructor (arg1, arg2) {
+ super()
+ this.nodeName = null
+ this._scrollElement = null
+ if (typeof arg1 === 'string') {
+ this.nodeName = arg1.toUpperCase()
+ } else if (arg1 != null && arg1.nodeType != null && arg1.nodeType === document.ELEMENT_NODE) {
+ this.nodeName = arg1.nodeName
+ this._setDom(arg1)
+ } else {
+ this.nodeName = 'UNDEFINED'
+ }
+ if (typeof arg2 === 'function') {
+ this._domFilter = arg2
+ }
+ }
+ _setDom (dom) {
+ if (this._dom != null) {
+ throw new Error('Only call this method if you know what you are doing ;)')
+ } else if (dom.__yxml != null) { // TODO do i need to check this? - no.. but for dev purps..
+ throw new Error('Already bound to an YXml type')
+ } else {
+ dom.__yxml = this
+ // tag is already set in constructor
+ // set attributes
+ let attrNames = []
+ for (let i = 0; i < dom.attributes.length; i++) {
+ attrNames.push(dom.attributes[i].name)
+ }
+ attrNames = this._domFilter(dom, attrNames)
+ for (let i = 0; i < attrNames.length; i++) {
+ let attrName = attrNames[i]
+ let attrValue = dom.getAttribute(attrName)
+ this.setAttribute(attrName, attrValue)
+ }
+ this.insertDomElements(0, Array.prototype.slice.call(dom.childNodes))
+ if (MutationObserver != null) {
+ this._dom = this._bindToDom(dom)
+ }
+ return dom
+ }
+ }
+ _fromBinary (y, decoder) {
+ const missing = super._fromBinary(y, decoder)
+ this.nodeName = decoder.readVarString()
+ return missing
+ }
+ _toBinary (encoder) {
+ super._toBinary(encoder)
+ encoder.writeVarString(this.nodeName)
+ }
+ _integrate (y) {
+ if (this.nodeName === null) {
+ throw new Error('nodeName must be defined!')
+ }
+ if (this._domFilter === defaultDomFilter && this._parent instanceof YXmlFragment) {
+ this._domFilter = this._parent._domFilter
+ }
+ super._integrate(y)
+ }
+ toString () {
+ const attrs = this.getAttributes()
+ const stringBuilder = []
+ for (let key in attrs) {
+ stringBuilder.push(key + '="' + attrs[key] + '"')
+ }
+ const nodeName = this.nodeName.toLocaleLowerCase()
+ const attrsString = stringBuilder.length > 0 ? ' ' + stringBuilder.join(' ') : ''
+ return `<${nodeName}${attrsString}>${super.toString()}${nodeName}>`
+ }
+ removeAttribute () {
+ return YMap.prototype.delete.apply(this, arguments)
+ }
+
+ setAttribute () {
+ return YMap.prototype.set.apply(this, arguments)
+ }
+
+ getAttribute () {
+ return YMap.prototype.get.apply(this, arguments)
+ }
+
+ getAttributes () {
+ const obj = {}
+ for (let [key, value] of this._map) {
+ obj[key] = value._content[0]
+ }
+ return obj
+ }
+ getDom () {
+ let dom = this._dom
+ if (dom == null) {
+ dom = document.createElement(this.nodeName)
+ dom.__yxml = this
+ let attrs = this.getAttributes()
+ for (let key in attrs) {
+ dom.setAttribute(key, attrs[key])
+ }
+ this.forEach(yxml => {
+ dom.appendChild(yxml.getDom())
+ })
+ if (MutationObserver !== null) {
+ this._dom = this._bindToDom(dom)
+ }
+ }
+ return dom
+ }
+}
diff --git a/src/Type/y-xml/YXmlFragment.js b/src/Type/y-xml/YXmlFragment.js
new file mode 100644
index 00000000..0ca986f5
--- /dev/null
+++ b/src/Type/y-xml/YXmlFragment.js
@@ -0,0 +1,167 @@
+/* global MutationObserver */
+
+import { defaultDomFilter, applyChangesFromDom, reflectChangesOnDom } from './utils.js'
+
+import YArray from '../YArray.js'
+import YXmlText from './YXmlText.js'
+
+function domToYXml (parent, doms) {
+ const types = []
+ doms.forEach(d => {
+ if (d.__yxml != null && d.__yxml !== false) {
+ d.__yxml._unbindFromDom()
+ }
+ if (parent._domFilter(d, []) !== null) {
+ let type
+ if (d.nodeType === document.TEXT_NODE) {
+ type = new YXmlText(d)
+ } else if (d.nodeType === document.ELEMENT_NODE) {
+ type = new YXmlFragment._YXmlElement(d, parent._domFilter)
+ } else {
+ throw new Error('Unsupported node!')
+ }
+ type.enableSmartScrolling(parent._scrollElement)
+ types.push(type)
+ } else {
+ d.__yxml = false
+ }
+ })
+ return types
+}
+
+export default class YXmlFragment extends YArray {
+ constructor () {
+ super()
+ this._dom = null
+ this._domFilter = defaultDomFilter
+ this._domObserver = null
+ // this function makes sure that either the
+ // dom event is executed, or the yjs observer is executed
+ var token = true
+ this._mutualExclude = f => {
+ if (token) {
+ token = false
+ try {
+ f()
+ } catch (e) {
+ console.error(e)
+ }
+ this._domObserver.takeRecords()
+ token = true
+ }
+ }
+ // Apply Y.Xml events to dom
+ this.observe(reflectChangesOnDom)
+ }
+ enableSmartScrolling (scrollElement) {
+ this._scrollElement = scrollElement
+ this.forEach(xml => {
+ xml.enableSmartScrolling(scrollElement)
+ })
+ }
+ setDomFilter (f) {
+ this._domFilter = f
+ this.forEach(xml => {
+ xml.setDomFilter(f)
+ })
+ }
+ _callObserver (parentSub) {
+ let event
+ if (parentSub !== null) {
+ event = {
+ type: 'attributeChanged',
+ name: parentSub,
+ value: this.getAttribute(parentSub),
+ target: this
+ }
+ } else {
+ event = {
+ type: 'contentChanged',
+ target: this
+ }
+ }
+ this._eventHandler.callEventListeners(event)
+ }
+ toString () {
+ return this.map(xml => xml.toString()).join('')
+ }
+ _unbindFromDom () {
+ if (this._domObserver != null) {
+ this._domObserver.disconnect()
+ this._domObserver = null
+ }
+ if (this._dom != null) {
+ this._dom.__yxml = null
+ this._dom = null
+ }
+ }
+ insertDomElementsAfter (prev, doms) {
+ const types = domToYXml(this, doms)
+ return this.insertAfter(prev, types)
+ }
+ insertDomElements (pos, doms) {
+ const types = domToYXml(this, doms)
+ this.insert(pos, types)
+ return types.length
+ }
+ bindToDom (dom) {
+ if (this._dom != null) {
+ this._unbindFromDom()
+ }
+ if (dom.__yxml != null) {
+ dom.__yxml._unbindFromDom()
+ }
+ if (MutationObserver == null) {
+ throw new Error('Not able to bind to a DOM element, because MutationObserver is not available!')
+ }
+ dom.innerHTML = ''
+ this.forEach(t => {
+ dom.insertBefore(t.getDom(), null)
+ })
+ this._dom = dom
+ dom.__yxml = this
+ this._bindToDom(dom)
+ }
+ // binds to a dom element
+ // Only call if dom and YXml are isomorph
+ _bindToDom (dom) {
+ this._domObserverListener = mutations => {
+ this._mutualExclude(() => {
+ let diffChildren = false
+ mutations.forEach(mutation => {
+ if (mutation.type === 'attributes') {
+ let name = mutation.attributeName
+ // check if filter accepts attribute
+ if (this._domFilter(this._dom, [name]).length > 0) {
+ var val = mutation.target.getAttribute(name)
+ if (this.getAttribute(name) !== val) {
+ if (val == null) {
+ this.removeAttribute(name)
+ } else {
+ this.setAttribute(name, val)
+ }
+ }
+ }
+ } else if (mutation.type === 'childList') {
+ diffChildren = true
+ }
+ })
+ if (diffChildren) {
+ applyChangesFromDom(this)
+ }
+ })
+ }
+ this._domObserver = new MutationObserver(this._domObserverListener)
+ const observeOptions = { childList: true }
+ if (this instanceof YXmlFragment._YXmlElement) {
+ observeOptions.attributes = true
+ }
+ this._domObserver.observe(dom, observeOptions)
+ return dom
+ }
+ _beforeChange () {
+ if (this._domObserver != null) {
+ this._domObserverListener(this._domObserver.takeRecords())
+ }
+ }
+}
diff --git a/src/Type/y-xml/YXmlText.js b/src/Type/y-xml/YXmlText.js
new file mode 100644
index 00000000..89a414bd
--- /dev/null
+++ b/src/Type/y-xml/YXmlText.js
@@ -0,0 +1,157 @@
+/* global getSelection, MutationObserver */
+
+import diff from 'fast-diff'
+import YText from '../YText.js'
+import { getAnchorViewPosition, fixScrollPosition, getBoundingClientRect } from './utils.js'
+
+function fixPosition (event, pos) {
+ if (event.index <= pos) {
+ if (event.type === 'delete') {
+ return pos - Math.min(pos - event.index, event.length)
+ } else {
+ return pos + 1
+ }
+ } else {
+ return pos
+ }
+}
+
+export default class YXmlText extends YText {
+ constructor (arg1) {
+ let dom = null
+ let initialText = null
+ if (arg1 != null && arg1.nodeType === document.TEXT_NODE) {
+ dom = arg1
+ initialText = dom.nodeValue
+ }
+ super(initialText)
+ this._dom = null
+ this._domObserver = null
+ this._domObserverListener = null
+ this._scrollElement = null
+ if (dom !== null) {
+ this._setDom(arg1)
+ }
+ var token = true
+ this._mutualExclude = f => {
+ if (token) {
+ token = false
+ try {
+ f()
+ } catch (e) {
+ console.error(e)
+ }
+ this._domObserver.takeRecords()
+ token = true
+ }
+ }
+ this.observe(event => {
+ if (this._dom != null) {
+ const dom = this._dom
+ this._mutualExclude(() => {
+ let selection = null
+ let shouldUpdateSelection = false
+ let anchorNode = null
+ let anchorOffset = null
+ let focusNode = null
+ let focusOffset = null
+ if (typeof getSelection !== 'undefined') {
+ selection = getSelection()
+ if (selection.anchorNode === dom) {
+ anchorNode = selection.anchorNode
+ anchorOffset = fixPosition(event, selection.anchorOffset)
+ shouldUpdateSelection = true
+ }
+ if (selection.focusNode === dom) {
+ focusNode = selection.focusNode
+ focusOffset = fixPosition(event, selection.focusOffset)
+ shouldUpdateSelection = true
+ }
+ }
+ let anchorViewPosition = getAnchorViewPosition(this._scrollElement)
+ let anchorViewFix
+ if (anchorViewPosition !== null && (anchorViewPosition.anchor !== null || getBoundingClientRect(this._dom).top <= 0)) {
+ anchorViewFix = anchorViewPosition
+ } else {
+ anchorViewFix = null
+ }
+ dom.nodeValue = this.toString()
+ fixScrollPosition(this._scrollElement, anchorViewFix)
+
+ if (shouldUpdateSelection) {
+ selection.setBaseAndExtent(
+ anchorNode || selection.anchorNode,
+ anchorOffset || selection.anchorOffset,
+ focusNode || selection.focusNode,
+ focusOffset || selection.focusOffset
+ )
+ }
+ })
+ }
+ })
+ }
+ setDomFilter () {}
+ enableSmartScrolling (scrollElement) {
+ this._scrollElement = scrollElement
+ }
+ _setDom (dom) {
+ if (this._dom != null) {
+ this._unbindFromDom()
+ }
+ if (dom.__yxml != null) {
+ dom.__yxml._unbindFromDom()
+ }
+ // set marker
+ this._dom = dom
+ dom.__yxml = this
+ if (typeof MutationObserver === 'undefined') {
+ return
+ }
+ this._domObserverListener = () => {
+ this._mutualExclude(() => {
+ var diffs = diff(this.toString(), dom.nodeValue)
+ var pos = 0
+ for (var i = 0; i < diffs.length; i++) {
+ var d = diffs[i]
+ if (d[0] === 0) { // EQUAL
+ pos += d[1].length
+ } else if (d[0] === -1) { // DELETE
+ this.delete(pos, d[1].length)
+ } else { // INSERT
+ this.insert(pos, d[1])
+ pos += d[1].length
+ }
+ }
+ })
+ }
+ this._domObserver = new MutationObserver(this._domObserverListener)
+ this._domObserver.observe(this._dom, { characterData: true })
+ }
+ getDom () {
+ if (this._dom == null) {
+ const dom = document.createTextNode(this.toString())
+ this._setDom(dom)
+ return dom
+ }
+ return this._dom
+ }
+ _beforeChange () {
+ if (this._domObserver != null && this._y !== null) { // TODO: do I need th y condition
+ this._domObserverListener(this._domObserver.takeRecords())
+ }
+ }
+ _delete (y, createDelete) {
+ this._unbindFromDom()
+ super._delete(y, createDelete)
+ }
+ _unbindFromDom () {
+ if (this._domObserver != null) {
+ this._domObserver.disconnect()
+ this._domObserver = null
+ }
+ if (this._dom != null) {
+ this._dom.__yxml = null
+ this._dom = null
+ }
+ }
+}
diff --git a/src/Type/y-xml/utils.js b/src/Type/y-xml/utils.js
new file mode 100644
index 00000000..70e40008
--- /dev/null
+++ b/src/Type/y-xml/utils.js
@@ -0,0 +1,206 @@
+
+export function defaultDomFilter (node, attributes) {
+ return attributes
+}
+
+export function getAnchorViewPosition (scrollElement) {
+ if (scrollElement == null) {
+ return null
+ }
+ let anchor = document.getSelection().anchorNode
+ if (anchor != null) {
+ let top = getBoundingClientRect(anchor).top
+ if (top >= 0 && top <= document.documentElement.clientHeight) {
+ return {
+ anchor: anchor,
+ top: top
+ }
+ }
+ }
+ return {
+ anchor: null,
+ scrollTop: scrollElement.scrollTop,
+ scrollHeight: scrollElement.scrollHeight
+ }
+}
+
+// get BoundingClientRect that works on text nodes
+export function getBoundingClientRect (element) {
+ if (element.getBoundingClientRect != null) {
+ // is element node
+ return element.getBoundingClientRect()
+ } else {
+ // is text node
+ if (element.parentNode == null) {
+ // range requires that text nodes have a parent
+ let span = document.createElement('span')
+ span.appendChild(element)
+ }
+ let range = document.createRange()
+ range.selectNode(element)
+ return range.getBoundingClientRect()
+ }
+}
+
+export function fixScrollPosition (scrollElement, fix) {
+ if (scrollElement !== null && fix !== null) {
+ if (fix.anchor === null) {
+ if (scrollElement.scrollTop === fix.scrollTop) {
+ scrollElement.scrollTop = scrollElement.scrollHeight - fix.scrollHeight
+ }
+ } else {
+ scrollElement.scrollTop = getBoundingClientRect(fix.anchor).top - fix.top
+ }
+ }
+}
+
+function iterateUntilUndeleted (item) {
+ while (item !== null && item._deleted) {
+ item = item._right
+ }
+ return item
+}
+
+/*
+ * 1. Check if any of the nodes was deleted
+ * 2. Iterate over the children.
+ * 2.1 If a node exists without __yxml property, insert a new node
+ * 2.2 If _contents.length < dom.childNodes.length, fill the
+ * rest of _content with childNodes
+ * 2.3 If a node was moved, delete it and
+ * recreate a new yxml element that is bound to that node.
+ * You can detect that a node was moved because expectedId
+ * !== actualId in the list
+ */
+export function applyChangesFromDom (yxml) {
+ const y = yxml._y
+ let knownChildren =
+ new Set(
+ Array.prototype.map.call(yxml._dom.childNodes, child => child.__yxml)
+ .filter(id => id !== undefined)
+ )
+ // 1. Check if any of the nodes was deleted
+ yxml.forEach(function (childType, i) {
+ if (!knownChildren.has(childType)) {
+ childType._delete(y)
+ }
+ })
+ // 2. iterate
+ let childNodes = yxml._dom.childNodes
+ let len = childNodes.length
+ let prevExpectedNode = null
+ let expectedNode = iterateUntilUndeleted(yxml._start)
+ for (let domCnt = 0; domCnt < len; domCnt++) {
+ const child = childNodes[domCnt]
+ const childYXml = child.__yxml
+ if (childYXml != null) {
+ if (childYXml === false) {
+ // should be ignored or is going to be deleted
+ continue
+ }
+ if (expectedNode !== null) {
+ if (expectedNode !== childYXml) {
+ // 2.3 Not expected node
+ if (childYXml._parent !== this) {
+ // element is going to be deleted by its previous parent
+ child.__yxml = null
+ } else {
+ childYXml._delete(y)
+ }
+ prevExpectedNode = yxml.insertDomElementsAfter(prevExpectedNode, [child])[0]
+ } else {
+ prevExpectedNode = expectedNode
+ expectedNode = iterateUntilUndeleted(expectedNode._right)
+ }
+ // if this is the expected node id, just continue
+ } else {
+ // 2.2 fill _conten with child nodes
+ prevExpectedNode = yxml.insertDomElementsAfter(prevExpectedNode, [child])[0]
+ }
+ } else {
+ // 2.1 A new node was found
+ prevExpectedNode = yxml.insertDomElementsAfter(prevExpectedNode, [child])[0]
+ }
+ }
+}
+
+export function reflectChangesOnDom (event) {
+ const yxml = event.target
+ const dom = yxml._dom
+ if (dom != null) {
+ yxml._mutualExclude(() => {
+ // TODO: do this once before applying stuff
+ // let anchorViewPosition = getAnchorViewPosition(yxml._scrollElement)
+ if (event.type === 'attributeChanged') {
+ if (event.value === undefined) {
+ dom.removeAttribute(event.name)
+ } else {
+ dom.setAttribute(event.name, event.value)
+ }
+ } else if (event.type === 'contentChanged') {
+ // create fragment of undeleted nodes
+ const fragment = document.createDocumentFragment()
+ yxml.forEach(function (t) {
+ fragment.append(t.getDom())
+ })
+ // remove remainding nodes
+ let lastChild = dom.lastChild
+ while (lastChild !== null) {
+ dom.removeChild(lastChild)
+ lastChild = dom.lastChild
+ }
+ // insert fragment of undeleted nodes
+ dom.append(fragment)
+ }
+ /* TODO: smartscrolling
+ .. else if (event.type === 'childInserted' || event.type === 'insert') {
+ let nodes = event.values
+ for (let i = nodes.length - 1; i >= 0; i--) {
+ let node = nodes[i]
+ node.setDomFilter(yxml._domFilter)
+ node.enableSmartScrolling(yxml._scrollElement)
+ let dom = node.getDom()
+ let fixPosition = null
+ let nextDom = null
+ if (yxml._content.length > event.index + i + 1) {
+ nextDom = yxml.get(event.index + i + 1).getDom()
+ }
+ yxml._dom.insertBefore(dom, nextDom)
+ if (anchorViewPosition === null) {
+ // nop
+ } else if (anchorViewPosition.anchor !== null) {
+ // no scrolling when current selection
+ if (!dom.contains(anchorViewPosition.anchor) && !anchorViewPosition.anchor.contains(dom)) {
+ fixPosition = anchorViewPosition
+ }
+ } else if (getBoundingClientRect(dom).top <= 0) {
+ // adjust scrolling if modified element is out of view,
+ // there is no anchor element, and the browser did not adjust scrollTop (this is checked later)
+ fixPosition = anchorViewPosition
+ }
+ fixScrollPosition(yxml._scrollElement, fixPosition)
+ }
+ } else if (event.type === 'childRemoved' || event.type === 'delete') {
+ for (let i = event.values.length - 1; i >= 0; i--) {
+ let dom = event.values[i]._dom
+ let fixPosition = null
+ if (anchorViewPosition === null) {
+ // nop
+ } else if (anchorViewPosition.anchor !== null) {
+ // no scrolling when current selection
+ if (!dom.contains(anchorViewPosition.anchor) && !anchorViewPosition.anchor.contains(dom)) {
+ fixPosition = anchorViewPosition
+ }
+ } else if (getBoundingClientRect(dom).top <= 0) {
+ // adjust scrolling if modified element is out of view,
+ // there is no anchor element, and the browser did not adjust scrollTop (this is checked later)
+ fixPosition = anchorViewPosition
+ }
+ dom.remove()
+ fixScrollPosition(yxml._scrollElement, fixPosition)
+ }
+ }
+ */
+ })
+ }
+}
diff --git a/src/Type/y-xml/y-xml.js b/src/Type/y-xml/y-xml.js
new file mode 100644
index 00000000..c7b15bac
--- /dev/null
+++ b/src/Type/y-xml/y-xml.js
@@ -0,0 +1,9 @@
+
+import YXmlFragment from './YXmlFragment.js'
+import YXmlElement from './YXmlElement.js'
+
+export { default as YXmlFragment } from './YXmlFragment.js'
+export { default as YXmlElement } from './YXmlElement.js'
+export { default as YXmlText } from './YXmlText.js'
+
+YXmlFragment._YXmlElement = YXmlElement
diff --git a/src/Util/deleteItemRange.js b/src/Util/deleteItemRange.js
deleted file mode 100644
index 1969a0d5..00000000
--- a/src/Util/deleteItemRange.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import Delete from '../Struct/Delete'
-import ID from './ID'
-
-export function deleteItemRange (y, user, clock, length) {
- let del = new Delete()
- del._target = new ID(user, clock)
- del._length = length
- del._integrate(y)
-}
diff --git a/src/Util/structReferences.js b/src/Util/structReferences.js
index df09d2f0..21d104c6 100644
--- a/src/Util/structReferences.js
+++ b/src/Util/structReferences.js
@@ -1,8 +1,11 @@
import YArray from '../Type/YArray.js'
import YMap from '../Type/YMap.js'
import YText from '../Type/YText.js'
-import YXml from '../Type/YXml.js'
+import YXmlFragment from '../Type/y-xml/YXmlFragment.js'
+import YXmlElement from '../Type/y-xml/YXmlElement.js'
+import YXmlText from '../Type/y-xml/YXmlText.js'
+import Delete from '../Struct/Delete.js'
import ItemJSON from '../Struct/ItemJSON.js'
import ItemString from '../Struct/ItemString.js'
@@ -22,9 +25,13 @@ export function getReference (typeConstructor) {
return references.get(typeConstructor)
}
-addStruct(0, YArray)
-addStruct(1, YMap)
-addStruct(2, YText)
-addStruct(3, YXml)
-addStruct(4, ItemJSON)
-addStruct(5, ItemString)
+addStruct(0, ItemJSON)
+addStruct(1, ItemString)
+addStruct(2, Delete)
+
+addStruct(3, YArray)
+addStruct(4, YMap)
+addStruct(5, YText)
+addStruct(6, YXmlFragment)
+addStruct(7, YXmlElement)
+addStruct(8, YXmlText)
diff --git a/src/Y.js b/src/Y.js
index fd013beb..fe183e0e 100644
--- a/src/Y.js
+++ b/src/Y.js
@@ -12,15 +12,22 @@ import Persistence from './Persistence.js'
import YArray from './Type/YArray.js'
import YMap from './Type/YMap.js'
import YText from './Type/YText.js'
-import YXml from './Type/YXml.js'
+import { YXmlFragment, YXmlElement, YXmlText } from './Type/y-xml/y-xml.js'
+import BinaryDecoder from './Binary/Decoder.js'
import debug from 'debug'
+function callTypesAfterTransaction (y) {
+ y._transactionChangedTypes.forEach(function (parentSub, type) {
+ type._callObserver(parentSub)
+ })
+}
+
export default class Y extends NamedEventHandler {
constructor (opts) {
super()
this._opts = opts
- this.userID = generateUserID()
+ this.userID = opts._userID != null ? opts._userID : generateUserID()
this.ds = new DeleteStore(this)
this.os = new OperationStore(this)
this.ss = new StateStore(this)
@@ -34,6 +41,27 @@ export default class Y extends NamedEventHandler {
this.connected = true
this._missingStructs = new Map()
this._readyToIntegrate = []
+ this._transactionsInProgress = 0
+ // types added during transaction
+ this._transactionNewTypes = new Set()
+ // changed types (does not include new types)
+ this._transactionChangedTypes = new Map()
+ this.on('afterTransaction', callTypesAfterTransaction)
+ }
+ _beforeChange () {}
+ transact (f) {
+ this._transactionsInProgress++
+ try {
+ f()
+ } catch (e) {
+ console.error(e)
+ }
+ this._transactionsInProgress--
+ if (this._transactionsInProgress === 0) {
+ this.emit('afterTransaction', this)
+ this._transactionChangedTypes = new Map()
+ this._transactionNewTypes = new Set()
+ }
}
// fake _start for root properties (y.set('name', type))
get _start () {
@@ -102,9 +130,13 @@ Y.Persisence = Persistence
Y.Array = YArray
Y.Map = YMap
Y.Text = YText
-Y.Xml = YXml
+Y.XmlElement = YXmlElement
+Y.XmlFragment = YXmlFragment
+Y.XmlText = YXmlText
-export { default as debug } from 'debug'
+Y.utils = {
+ BinaryDecoder
+}
Y.debug = debug
debug.formatters.Y = messageToString
diff --git a/src/y-dist.cjs.js b/src/y-dist.cjs.js
new file mode 100644
index 00000000..1168bd24
--- /dev/null
+++ b/src/y-dist.cjs.js
@@ -0,0 +1,3 @@
+
+import Y from './Y.js'
+export default Y
diff --git a/test/y-array.tests.js b/test/y-array.tests.js
index 69c15a11..1e1d48f6 100644
--- a/test/y-array.tests.js
+++ b/test/y-array.tests.js
@@ -27,9 +27,9 @@ test('basic spec', async function array0 (t) {
test('insert three elements, try re-get property', async function array1 (t) {
var { users, array0, array1 } = await initArrays(t, { users: 2 })
array0.insert(0, [1, 2, 3])
- t.compare(array0.toArray(), [1, 2, 3], '.toArray() works')
+ t.compare(array0.toJSON(), [1, 2, 3], '.toJSON() works')
await flushAll(t, users)
- t.compare(array1.toArray(), [1, 2, 3], '.toArray() works after sync')
+ t.compare(array1.toJSON(), [1, 2, 3], '.toJSON() works after sync')
await compareUsers(t, users)
})
@@ -76,8 +76,8 @@ test('disconnect really prevents sending messages', async function array5 (t) {
array0.insert(1, ['user0'])
array1.insert(1, ['user1'])
await wait(1000)
- t.compare(array0.toArray(), ['x', 'user0', 'y'])
- t.compare(array1.toArray(), ['x', 'user1', 'y'])
+ t.compare(array0.toJSON(), ['x', 'user0', 'y'])
+ t.compare(array1.toJSON(), ['x', 'user1', 'y'])
await users[1].reconnect()
await users[2].reconnect()
await compareUsers(t, users)
@@ -225,7 +225,7 @@ test('event has correct value when setting a primitive on a YArray (same user)',
array0.insert(0, ['stuff'])
t.assert(event.values[0] === event.object.get(0), 'compare value with get method')
t.assert(event.values[0] === 'stuff', 'check that value is actually present')
- t.assert(event.values[0] === array0.toArray()[0], '.toArray works as expected')
+ t.assert(event.values[0] === array0.toJSON()[0], '.toJSON works as expected')
await compareUsers(t, users)
})
@@ -240,7 +240,7 @@ test('event has correct value when setting a primitive on a YArray (received fro
await flushAll(t, users)
t.assert(event.values[0] === event.object.get(0), 'compare value with get method')
t.assert(event.values[0] === 'stuff', 'check that value is actually present')
- t.assert(event.values[0] === array0.toArray()[0], '.toArray works as expected')
+ t.assert(event.values[0] === array0.toJSON()[0], '.toJSON works as expected')
await compareUsers(t, users)
})
@@ -254,7 +254,7 @@ test('event has correct value when setting a type on a YArray (same user)', asyn
array0.insert(0, [Y.Array])
t.assert(event.values[0] === event.object.get(0), 'compare value with get method')
t.assert(event.values[0] != null, 'event.value exists')
- t.assert(event.values[0] === array0.toArray()[0], '.toArray works as expected')
+ t.assert(event.values[0] === array0.toJSON()[0], '.toJSON works as expected')
await compareUsers(t, users)
})
test('event has correct value when setting a type on a YArray (ops received from another user)', async function array14 (t) {
@@ -268,7 +268,7 @@ test('event has correct value when setting a type on a YArray (ops received from
await flushAll(t, users)
t.assert(event.values[0] === event.object.get(0), 'compare value with get method')
t.assert(event.values[0] != null, 'event.value exists')
- t.assert(event.values[0] === array0.toArray()[0], '.toArray works as expected')
+ t.assert(event.values[0] === array0.toJSON()[0], '.toJSON works as expected')
await compareUsers(t, users)
})
diff --git a/test/y-map.tests.js b/test/y-map.tests.js
index 9b424f4a..5a07fd1f 100644
--- a/test/y-map.tests.js
+++ b/test/y-map.tests.js
@@ -10,9 +10,9 @@ test('basic map tests', async function map0 (t) {
map0.set('number', 1)
map0.set('string', 'hello Y')
map0.set('object', { key: { key2: 'value' } })
- map0.set('y-map', Y.Map)
+ map0.set('y-map', new Y.Map())
let map = map0.get('y-map')
- map.set('y-array', Y.Array)
+ map.set('y-array', new Y.Array())
let array = map.get('y-array')
array.insert(0, [0])
array.insert(0, [-1])
@@ -46,7 +46,7 @@ test('Basic get&set of Map property (converge via sync)', async function map1 (t
await flushAll(t, users)
for (let user of users) {
- var u = user.share.map
+ var u = user.get('map', Y.Map)
t.compare(u.get('stuff'), 'stuffy')
}
await compareUsers(t, users)
@@ -54,7 +54,7 @@ test('Basic get&set of Map property (converge via sync)', async function map1 (t
test('Map can set custom types (Map)', async function map2 (t) {
let { users, map0 } = await initArrays(t, { users: 2 })
- var map = map0.set('Map', Y.Map)
+ var map = map0.set('Map', new Y.Map())
map.set('one', 1)
map = map0.get('Map')
t.compare(map.get('one'), 1)
@@ -63,7 +63,7 @@ test('Map can set custom types (Map)', async function map2 (t) {
test('Map can set custom types (Map) - get also returns the type', async function map3 (t) {
let { users, map0 } = await initArrays(t, { users: 2 })
- map0.set('Map', Y.Map)
+ map0.set('Map', new Y.Map())
var map = map0.get('Map')
map.set('one', 1)
map = map0.get('Map')
@@ -73,7 +73,7 @@ test('Map can set custom types (Map) - get also returns the type', async functio
test('Map can set custom types (Array)', async function map4 (t) {
let { users, map0 } = await initArrays(t, { users: 2 })
- var array = map0.set('Array', Y.Array)
+ var array = map0.set('Array', new Y.Array())
array.insert(0, [1, 2, 3])
array = map0.get('Array')
t.compare(array.toArray(), [1, 2, 3])
@@ -88,7 +88,7 @@ test('Basic get&set of Map property (converge via update)', async function map5
await flushAll(t, users)
for (let user of users) {
- var u = user.share.map
+ var u = user.get('map', Y.Map)
t.compare(u.get('stuff'), 'stuffy')
}
await compareUsers(t, users)
@@ -102,7 +102,7 @@ test('Basic get&set of Map property (handle conflict)', async function map6 (t)
await flushAll(t, users)
for (let user of users) {
- var u = user.share.map
+ var u = user.get('map', Y.Map)
t.compare(u.get('stuff'), 'c0')
}
await compareUsers(t, users)
@@ -115,7 +115,7 @@ test('Basic get&set&delete of Map property (handle conflict)', async function ma
map1.set('stuff', 'c1')
await flushAll(t, users)
for (let user of users) {
- var u = user.share.map
+ var u = user.get('map', Y.Map)
t.assert(u.get('stuff') === undefined)
}
await compareUsers(t, users)
@@ -129,7 +129,7 @@ test('Basic get&set of Map property (handle three conflicts)', async function ma
map2.set('stuff', 'c3')
await flushAll(t, users)
for (let user of users) {
- var u = user.share.map
+ var u = user.get('map', Y.Map)
t.compare(u.get('stuff'), 'c0')
}
await compareUsers(t, users)
@@ -149,7 +149,7 @@ test('Basic get&set&delete of Map property (handle three conflicts)', async func
map3.set('stuff', 'c3')
await flushAll(t, users)
for (let user of users) {
- var u = user.share.map
+ var u = user.get('map', Y.Map)
t.assert(u.get('stuff') === undefined)
}
await compareUsers(t, users)
@@ -163,7 +163,7 @@ test('observePath properties', async function map10 (t) {
map.set('yay', 4)
}
})
- map1.set('map', Y.Map)
+ map1.set('map', new Y.Map())
await flushAll(t, users)
map = map2.get('map')
t.compare(map.get('yay'), 4)
@@ -172,7 +172,7 @@ test('observePath properties', async function map10 (t) {
test('observe deep properties', async function map11 (t) {
let { users, map1, map2, map3 } = await initArrays(t, { users: 4 })
- var _map1 = map1.set('map', Y.Map)
+ var _map1 = map1.set('map', new Y.Map())
var calls = 0
var dmapid
_map1.observe(function (event) {
@@ -182,10 +182,10 @@ test('observe deep properties', async function map11 (t) {
})
await flushAll(t, users)
var _map3 = map3.get('map')
- _map3.set('deepmap', Y.Map)
+ _map3.set('deepmap', new Y.Map())
await flushAll(t, users)
var _map2 = map2.get('map')
- _map2.set('deepmap', Y.Map)
+ _map2.set('deepmap', new Y.Map())
await flushAll(t, users)
var dmap1 = _map1.get('deepmap')
var dmap2 = _map2.get('deepmap')
@@ -205,8 +205,8 @@ test('observes using observePath', async function map12 (t) {
pathes.push(event.path)
calls++
})
- map0.set('map', Y.Map)
- map0.get('map').set('array', Y.Array)
+ map0.set('map', new Y.Map())
+ map0.get('map').set('array', new Y.Array())
map0.get('map').get('array').insert(0, ['content'])
t.assert(calls === 3)
t.compare(pathes, [[], ['map'], ['map', 'array']])
@@ -233,7 +233,7 @@ test('throws add & update & delete events (with type and primitive content)', as
name: 'stuff'
})
// update, oldValue is in contents
- map0.set('stuff', Y.Array)
+ map0.set('stuff', new Y.Array())
compareEvent(t, event, {
type: 'update',
object: map0,
@@ -288,7 +288,7 @@ test('event has correct value when setting a type on a YMap (same user)', async
map0.observe(function (e) {
event = e
})
- map0.set('stuff', Y.Map)
+ map0.set('stuff', new Y.Map())
t.compare(event.value._model, event.object.get(event.name)._model)
await compareUsers(t, users)
})
@@ -300,7 +300,7 @@ test('event has correct value when setting a type on a YMap (ops received from a
map0.observe(function (e) {
event = e
})
- map1.set('stuff', Y.Map)
+ map1.set('stuff', new Y.Map())
await flushAll(t, users)
t.compare(event.value._model, event.object.get(event.name)._model)
await compareUsers(t, users)
@@ -310,13 +310,13 @@ var mapTransactions = [
function set (t, user, chance) {
let key = chance.pickone(['one', 'two'])
var value = chance.string()
- user.share.map.set(key, value)
+ user.get('map', Y.Map).set(key, value)
},
function setType (t, user, chance) {
let key = chance.pickone(['one', 'two'])
- var value = chance.pickone([Y.Array, Y.Map])
- let type = user.share.map.set(key, value)
- if (value === Y.Array) {
+ var value = chance.pickone([new Y.Array(), new Y.Map()])
+ let type = user.get('map', Y.Map).set(key, value)
+ if (value === new Y.Array()) {
type.insert(0, [1, 2, 3, 4])
} else {
type.set('deepkey', 'deepvalue')
@@ -324,7 +324,7 @@ var mapTransactions = [
},
function _delete (t, user, chance) {
let key = chance.pickone(['one', 'two'])
- user.share.map.delete(key)
+ user.get('map', Y.Map).delete(key)
}
]
diff --git a/test/y-xml.tests.js b/test/y-xml.tests.js
index 9c832575..1299b25d 100644
--- a/test/y-xml.tests.js
+++ b/test/y-xml.tests.js
@@ -50,7 +50,7 @@ test('events', async function xml1 (t) {
type: 'childInserted',
index: 0
}
- xml0.insert(0, [Y.XmlText('some text')])
+ xml0.insert(0, [new Y.XmlText('some text')])
t.compare(event, expectedEvent, 'child inserted event')
await flushAll(t, users)
t.compare(remoteEvent, expectedEvent, 'child inserted event (remote)')
@@ -110,8 +110,8 @@ test('element insert (dom -> y)', async function xml4 (t) {
test('element insert (y -> dom)', async function xml5 (t) {
var { users, xml0 } = await initArrays(t, { users: 3 })
let dom0 = xml0.getDom()
- xml0.insert(0, [Y.XmlText('some text')])
- xml0.insert(1, [Y.XmlElement('p')])
+ xml0.insert(0, [new Y.XmlText('some text')])
+ xml0.insert(1, [new Y.XmlElement('p')])
t.assert(dom0.childNodes[0].textContent === 'some text', 'Retrieve Text node')
t.assert(dom0.childNodes[1].nodeName === 'P', 'Retrieve Element node')
await compareUsers(t, users)
@@ -132,7 +132,7 @@ test('y on insert, then delete (dom -> y)', async function xml6 (t) {
test('y on insert, then delete (y -> dom)', async function xml7 (t) {
var { users, xml0 } = await initArrays(t, { users: 3 })
let dom0 = xml0.getDom()
- xml0.insert(0, [Y.XmlElement('p')])
+ xml0.insert(0, [new Y.XmlElement('p')])
t.assert(dom0.childNodes[0].nodeName === 'P', 'Get inserted element from dom')
xml0.delete(0, 1)
t.assert(dom0.childNodes.length === 0, '#childNodes is empty after delete')
@@ -142,7 +142,7 @@ test('y on insert, then delete (y -> dom)', async function xml7 (t) {
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))
+ xml0.insert(0, [new Y.XmlText('1'), new Y.XmlText('2'), new Y.XmlText('3')])
await wait()
xml0.delete(1, 2)
await wait()
@@ -155,7 +155,7 @@ test('delete consecutive (1) (Text)', async function xml8 (t) {
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))
+ xml0.insert(0, [new Y.XmlText('1'), new Y.XmlText('2'), new Y.XmlText('3')])
await wait()
xml0.delete(0, 1)
xml0.delete(1, 1)
@@ -169,7 +169,7 @@ test('delete consecutive (2) (Text)', async function xml9 (t) {
test('delete consecutive (1) (Element)', async function xml10 (t) {
var { users, xml0 } = await initArrays(t, { users: 3 })
let dom0 = xml0.getDom()
- xml0.insert(0, [Y.XmlElement('A'), Y.XmlElement('B'), Y.XmlElement('C')])
+ xml0.insert(0, [new Y.XmlElement('A'), new Y.XmlElement('B'), new Y.XmlElement('C')])
await wait()
xml0.delete(1, 2)
await wait()
@@ -182,7 +182,7 @@ test('delete consecutive (1) (Element)', async function xml10 (t) {
test('delete consecutive (2) (Element)', async function xml11 (t) {
var { users, xml0 } = await initArrays(t, { users: 3 })
let dom0 = xml0.getDom()
- xml0.insert(0, [Y.XmlElement('A'), Y.XmlElement('B'), Y.XmlElement('C')])
+ xml0.insert(0, [new Y.XmlElement('A'), new Y.XmlElement('B'), new Y.XmlElement('C')])
await wait()
xml0.delete(0, 1)
xml0.delete(1, 1)
@@ -198,8 +198,8 @@ test('Receive a bunch of elements (with disconnect)', async function xml12 (t) {
let dom0 = xml0.getDom()
let dom1 = xml1.getDom()
users[1].disconnect()
- xml0.insert(0, [Y.XmlElement('A'), Y.XmlElement('B'), Y.XmlElement('C')])
- xml0.insert(0, [Y.XmlElement('X'), Y.XmlElement('Y'), Y.XmlElement('Z')])
+ xml0.insert(0, [new Y.XmlElement('A'), new Y.XmlElement('B'), new Y.XmlElement('C')])
+ xml0.insert(0, [new Y.XmlElement('X'), new Y.XmlElement('Y'), new Y.XmlElement('Z')])
await users[1].reconnect()
await flushAll(t, users)
t.assert(xml0.length === 6, 'check length (y)')
@@ -267,36 +267,37 @@ test('filter attribute', async function xml15 (t) {
// TODO: move elements
var xmlTransactions = [
- function attributeChange (t, user, chance) {
- user.share.xml.getDom().setAttribute(chance.word(), chance.word())
+ /*function attributeChange (t, user, chance) {
+ user.get('xml', Y.XmlElement).getDom().setAttribute(chance.word(), chance.word())
},
function attributeChangeHidden (t, user, chance) {
- user.share.xml.getDom().setAttribute('hidden', chance.word())
- },
+ user.get('xml', Y.XmlElement).getDom().setAttribute('hidden', chance.word())
+ },*/
function insertText (t, user, chance) {
- let dom = user.share.xml.getDom()
+ let dom = user.get('xml', Y.XmlElement).getDom()
var succ = dom.children.length > 0 ? chance.pickone(dom.children) : null
dom.insertBefore(document.createTextNode(chance.word()), succ)
- },
+ },/*
function insertHiddenDom (t, user, chance) {
- let dom = user.share.xml.getDom()
+ let dom = user.get('xml', Y.XmlElement).getDom()
var succ = dom.children.length > 0 ? chance.pickone(dom.children) : null
dom.insertBefore(document.createElement('hidden'), succ)
},
+ /*
function insertDom (t, user, chance) {
- let dom = user.share.xml.getDom()
+ let dom = user.get('xml', Y.XmlElement).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()
+ let dom = user.get('xml', Y.XmlElement).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()
+ let dom = user.get('xml', Y.XmlElement).getDom()
if (dom.children.length > 0) {
let dom2 = chance.pickone(dom.children)
let succ = dom2.childNodes.length > 0 ? chance.pickone(dom2.childNodes) : null
@@ -304,7 +305,7 @@ var xmlTransactions = [
}
},
function insertDomSecondLayer (t, user, chance) {
- let dom = user.share.xml.getDom()
+ let dom = user.get('xml', Y.XmlElement).getDom()
if (dom.children.length > 0) {
let dom2 = chance.pickone(dom.children)
let succ = dom2.childNodes.length > 0 ? chance.pickone(dom2.childNodes) : null
@@ -312,7 +313,7 @@ var xmlTransactions = [
}
},
function deleteChildSecondLayer (t, user, chance) {
- let dom = user.share.xml.getDom()
+ let dom = user.get('xml', Y.XmlElement).getDom()
if (dom.children.length > 0) {
let dom2 = chance.pickone(dom.children)
if (dom2.childNodes.length > 0) {
@@ -320,7 +321,7 @@ var xmlTransactions = [
d.remove()
}
}
- }
+ }*/
]
test('y-xml: Random tests (10)', async function xmlRandom10 (t) {
diff --git a/tests-lib/helper.js b/tests-lib/helper.js
index f6c87145..8d148ec0 100644
--- a/tests-lib/helper.js
+++ b/tests-lib/helper.js
@@ -3,6 +3,8 @@ import _Y from '../src/Y.js'
import yTest from './test-connector.js'
import Chance from 'chance'
+import ItemJSON from '../src/Struct/ItemJSON.js'
+import ItemString from '../src/Struct/ItemString.js'
export const Y = _Y
@@ -22,8 +24,8 @@ function getStateSet (y) {
function getDeleteSet (y) {
var ds = {}
y.ds.iterate(null, null, function (n) {
- var user = n.id[0]
- var counter = n.id[1]
+ var user = n._id.user
+ var counter = n._id.clock
var len = n.len
var gc = n.gc
var dv = ds[user]
@@ -112,12 +114,17 @@ export async function compareUsers (t, users) {
let ops = []
u.os.iterate(null, null, function (op) {
if (!op._deleted) {
- ops.push({
+ const json = {
id: op._id,
- left: op._left,
- right: op._right,
+ left: op._left === null ? null : op._left._id,
+ right: op._right === null ? null : op._right._id,
+ length: op._length,
deleted: op._deleted
- })
+ }
+ if (op instanceof ItemJSON || op instanceof ItemString) {
+ json.content = op._content
+ }
+ ops.push(json)
}
})
data.os = ops
@@ -152,10 +159,13 @@ export async function initArrays (t, opts) {
connOpts = Object.assign({ role: 'slave' }, conn)
}
let y = new Y({
+ _userID: i, // evil hackery, don't try this at home
connector: connOpts
})
result.users.push(y)
result['array' + i] = y.get('array', Y.Array)
+ result['map' + i] = y.get('map', Y.Map)
+ result['xml' + i] = y.get('xml', Y.XmlElement)
y.get('xml', Y.Xml).setDomFilter(function (d, attrs) {
if (d.nodeName === 'HIDDEN') {
return null
diff --git a/tests-lib/test-connector.js b/tests-lib/test-connector.js
index f20d3654..17fe19f5 100644
--- a/tests-lib/test-connector.js
+++ b/tests-lib/test-connector.js
@@ -136,7 +136,6 @@ export default function extendTestConnector (Y) {
// this one needs to sync with every other user
flushUsers = Array.from(this.connections.keys()).map(uid => this.testRoom.users.get(uid).y)
}
- var finished = []
for (let i = 0; i < flushUsers.length; i++) {
let userID = flushUsers[i].connector.y.userID
if (userID !== this.y.userID && this.connections.has(userID)) {
@@ -144,14 +143,12 @@ export default function extendTestConnector (Y) {
if (buffer != null) {
var messages = buffer.splice(0)
for (let j = 0; j < messages.length; j++) {
- let p = super.receiveMessage(userID, messages[j])
- finished.push(p)
+ super.receiveMessage(userID, messages[j])
}
}
}
}
- await Promise.all(finished)
- return finished.length > 0 ? 'flushing' : 'done'
+ return 'done'
}
}
// TODO: this should be moved to a separate module (dont work on Y)