From 755c9eb16e5049162a7c8eb49663379dbf7810ba Mon Sep 17 00:00:00 2001 From: Kevin Jahns Date: Thu, 19 Oct 2017 17:36:28 +0200 Subject: [PATCH] implemented xml type for new event system --- examples/html-editor/index.js | 18 +- examples/yjs-dist.esm | 9 +- package-lock.json | 5 + package.json | 1 + rollup.node.js | 4 +- rollup.test.js | 2 +- src/Connector.js | 10 +- src/MessageHandler/deleteSet.js | 102 ++++----- src/MessageHandler/integrateRemoteStructs.js | 37 ++-- src/MessageHandler/messageToString.js | 8 +- src/MessageHandler/syncStep1.js | 14 +- src/MessageHandler/syncStep2.js | 4 +- src/MessageHandler/update.js | 4 +- src/Store/DeleteStore.js | 47 ++--- src/Store/OperationStore.js | 8 +- src/Store/StateStore.js | 12 +- src/Struct/Delete.js | 33 ++- src/Struct/Item.js | 81 ++++++-- src/Struct/ItemJSON.js | 3 +- src/Struct/Type.js | 44 +++- src/Type/YArray.js | 171 +++++++++------ src/Type/YMap.js | 47 +++-- src/Type/YText.js | 49 +++++ src/Type/YXml.js | 10 - src/Type/y-xml/YXmlElement.js | 117 +++++++++++ src/Type/y-xml/YXmlFragment.js | 167 +++++++++++++++ src/Type/y-xml/YXmlText.js | 157 ++++++++++++++ src/Type/y-xml/utils.js | 206 +++++++++++++++++++ src/Type/y-xml/y-xml.js | 9 + src/Util/deleteItemRange.js | 9 - src/Util/structReferences.js | 21 +- src/Y.js | 40 +++- src/y-dist.cjs.js | 3 + test/y-array.tests.js | 16 +- test/y-map.tests.js | 50 ++--- test/y-xml.tests.js | 47 ++--- tests-lib/helper.js | 22 +- tests-lib/test-connector.js | 7 +- 38 files changed, 1272 insertions(+), 322 deletions(-) delete mode 100644 src/Type/YXml.js create mode 100644 src/Type/y-xml/YXmlElement.js create mode 100644 src/Type/y-xml/YXmlFragment.js create mode 100644 src/Type/y-xml/YXmlText.js create mode 100644 src/Type/y-xml/utils.js create mode 100644 src/Type/y-xml/y-xml.js delete mode 100644 src/Util/deleteItemRange.js create mode 100644 src/y-dist.cjs.js 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()}` + } + 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)