Compare commits

..

13 Commits

Author SHA1 Message Date
Kevin Jahns
eeacb5665a v13.0.0-21 -- distribution files 2017-10-02 15:52:23 +02:00
Kevin Jahns
c8ca80d15f 13.0.0-21 2017-10-02 15:52:11 +02:00
Kevin Jahns
be282c8338 fix lint 2017-10-02 15:50:56 +02:00
Kevin Jahns
829a094c6d check for responsiveness when maxBufferSize is set 2017-10-02 15:45:23 +02:00
Kevin Jahns
725273167e 13.0.0-20 2017-09-29 22:34:18 +02:00
Kevin Jahns
581264c5e3 implement relative position helper 2017-09-29 22:33:28 +02:00
Kevin Jahns
be537c9f8c 13.0.0-19 2017-09-26 21:53:01 +02:00
Kevin Jahns
4028eee39d implemented chunked broadcast of updates 2017-09-26 21:52:07 +02:00
Kevin Jahns
0e3e561ec7 13.0.0-18 2017-09-20 11:34:03 +02:00
Kevin Jahns
7df46cb731 Merge branch 'master' of github.com:y-js/yjs 2017-09-20 11:30:24 +02:00
Kevin Jahns
40fb16ef32 catch y-* related errors 2017-09-20 11:29:13 +02:00
Kevin Jahns
ada5d36cd5 add more y-xml tests 2017-09-19 03:16:48 +02:00
Kevin Jahns
f537a43e29 implement tests for dom filter 2017-09-18 22:14:45 +02:00
16 changed files with 924 additions and 1053 deletions

View File

@@ -7,9 +7,9 @@ Y({
},
connector: {
name: 'websockets-client',
// url: 'http://127.0.0.1:1234',
url: 'http://192.168.178.81:1234',
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"

View File

@@ -4,8 +4,7 @@
<textarea style="width:80%;" rows=40 id="textfield" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
<script src="../../y.js"></script>
<script src="../../../y-array/y-array.js"></script>
<script src="../../../y-text/dist/y-text.js"></script>
<script src="../../../y-memory/y-memory.js"></script>
<script src="../../../y-text/y-text.js"></script>
<script src="../../../y-websockets-client/y-websockets-client.js"></script>
<script src="./index.js"></script>
</body>

View File

@@ -1,8 +1,5 @@
/* global Y */
// eslint-disable-next-line
let search = new URLSearchParams(location.search)
// initialize a shared object. This function call returns a promise!
Y({
db: {
@@ -10,10 +7,9 @@ Y({
},
connector: {
name: 'websockets-client',
room: 'Textarea-example',
room: 'Textarea-example2',
// url: '//localhost:1234',
url: 'https://yjs-v13.herokuapp.com/'
// options: { transports: ['websocket'], upgrade: false }
},
share: {
textarea: 'Text'
@@ -24,7 +20,4 @@ Y({
// bind the textarea to a shared text element
y.share.textarea.bind(document.getElementById('textfield'))
// thats it..
}).catch(() => {
console.log('Something went wrong while creating the instance..')
})

View File

@@ -1,6 +1,6 @@
{
"name": "yjs",
"version": "13.0.0-17",
"version": "13.0.0-21",
"description": "A framework for real-time p2p shared editing on any data",
"main": "./y.node.js",
"browser": "./y.js",

View File

@@ -42,6 +42,11 @@ export default function extendConnector (Y/* :any */) {
if (opts.generateUserId !== false) {
this.setUserId(Y.utils.generateUserId())
}
if (opts.maxBufferLength == null) {
this.maxBufferLength = -1
} else {
this.maxBufferLength = opts.maxBufferLength
}
}
reconnect () {
@@ -197,14 +202,19 @@ export default function extendConnector (Y/* :any */) {
encoder.writeVarString(self.opts.room)
encoder.writeVarString('update')
let ops = self.broadcastOpBuffer
self.broadcastOpBuffer = []
let length = ops.length
encoder.writeUint32(length)
for (var i = 0; i < length; i++) {
let encoderPosLen = encoder.pos
encoder.writeUint32(0)
for (var i = 0; i < length && (self.maxBufferLength < 0 || encoder.length < self.maxBufferLength); i++) {
let op = ops[i]
Y.Struct[op.struct].binaryEncode(encoder, op)
}
encoder.setUint32(encoderPosLen, i)
self.broadcastOpBuffer = ops.slice(i)
self.broadcast(encoder.createBuffer())
if (i !== length) {
self.whenRemoteResponsive().then(broadcastOperations)
}
}
}
if (this.broadcastOpBuffer.length === 0) {
@@ -215,6 +225,20 @@ export default function extendConnector (Y/* :any */) {
}
}
/*
* Somehow check the responsiveness of the remote clients/server
* Default behavior:
* Wait 100ms before broadcasting the next batch of operations
*
* Only used when maxBufferLength is set
*
*/
whenRemoteResponsive () {
return new Promise(function (resolve) {
setTimeout(resolve, 100)
})
}
/*
You received a raw message, and you know that it is intended for Yjs. Then call this function.
*/

View File

@@ -8,6 +8,10 @@ export class BinaryEncoder {
this.data = []
}
get length () {
return this.data.length
}
get pos () {
return this.data.length
}

View File

@@ -49,6 +49,51 @@ export default function Utils (Y) {
}
}
Y.utils.getRelativePosition = function (type, offset) {
if (type == null) {
return null
} else {
if (type._content.length <= offset) {
return ['endof', type._model[0], type._model[1]]
} else {
return type._content[offset].id
}
}
}
Y.utils.fromRelativePosition = function (y, id) {
var offset = 0
var op
if (id[0] === 'endof') {
id = y.db.os.find(id.slice(1)).end
op = y.db.os.findNodeWithUpperBound(id).val
if (!op.deleted) {
offset = op.content != null ? op.content.length : 1
}
} else {
op = y.db.os.findNodeWithUpperBound(id).val
if (!op.deleted) {
offset = id[1] - op.id[1]
}
}
var type = y.db.getType(op.parent)
if (type == null || y.db.os.find(op.parent).deleted) {
return null
}
while (op.left != null) {
op = y.db.os.findNodeWithUpperBound(op.left).val
if (!op.deleted) {
offset += op.content != null ? op.content.length : 1
}
}
return {
type: type,
offset: offset
}
}
class NamedEventHandler {
constructor () {
this._eventListener = {}
@@ -67,7 +112,11 @@ export default function Utils (Y) {
this._eventListener[name] = listener.filter(e => e !== f)
}
emit (name, value) {
(this._eventListener[name] || []).forEach(l => l(value))
let listener = this._eventListener[name] || []
if (name === 'error' && listener.length === 0) {
console.error(value)
}
listener.forEach(l => l(value))
}
destroy () {
this._eventListener = null

View File

@@ -1,4 +1,3 @@
import extendRBTree from './RedBlackTree'
export default function extend (Y) {
@@ -48,9 +47,13 @@ export default function extend (Y) {
}
transact (makeGen) {
const t = new Transaction(this)
while (makeGen != null) {
makeGen.call(t)
makeGen = this.getNextRequest()
try {
while (makeGen != null) {
makeGen.call(t)
makeGen = this.getNextRequest()
}
} catch (e) {
this.y.emit('error', e)
}
}
destroy () {

View File

@@ -224,16 +224,65 @@ test('move element to a different position', async function xml13 (t) {
await compareUsers(t, users)
})
test('filter node', async function xml14 (t) {
var { users, xml0, xml1 } = await initArrays(t, { users: 3 })
let dom0 = xml0.getDom()
let dom1 = xml1.getDom()
let domFilter = (node, attrs) => {
if (node.nodeName === 'H1') {
return null
} else {
return attrs
}
}
xml0.setDomFilter(domFilter)
xml1.setDomFilter(domFilter)
dom0.append(document.createElement('div'))
dom0.append(document.createElement('h1'))
await flushAll(t, users)
t.assert(dom1.childNodes.length === 1, 'Only one node was not transmitted')
t.assert(dom1.childNodes[0].nodeName === 'DIV', 'div node was transmitted')
await compareUsers(t, users)
})
test('filter attribute', async function xml15 (t) {
var { users, xml0, xml1 } = await initArrays(t, { users: 3 })
let dom0 = xml0.getDom()
let dom1 = xml1.getDom()
let domFilter = (node, attrs) => {
return attrs.filter(name => name !== 'hidden')
}
xml0.setDomFilter(domFilter)
xml1.setDomFilter(domFilter)
dom0.setAttribute('hidden', 'true')
dom0.setAttribute('style', 'height: 30px')
dom0.setAttribute('data-me', '77')
await flushAll(t, users)
t.assert(dom0.getAttribute('hidden') === 'true', 'User 0 still has the attribute')
t.assert(dom1.getAttribute('hidden') == null, 'User 1 did not receive update')
t.assert(dom1.getAttribute('style') === 'height: 30px', 'User 1 received style update')
t.assert(dom1.getAttribute('data-me') === '77', 'User 1 received data update')
await compareUsers(t, users)
})
// TODO: move elements
var xmlTransactions = [
function attributeChange (t, user, chance) {
user.share.xml.getDom().setAttribute(chance.word(), chance.word())
},
function attributeChangeHidden (t, user, chance) {
user.share.xml.getDom().setAttribute('hidden', chance.word())
},
function insertText (t, user, chance) {
let dom = user.share.xml.getDom()
var succ = dom.children.length > 0 ? chance.pickone(dom.children) : null
dom.insertBefore(document.createTextNode(chance.word()), succ)
},
function insertHiddenDom (t, user, chance) {
let dom = user.share.xml.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()
var succ = dom.children.length > 0 ? chance.pickone(dom.children) : null
@@ -274,30 +323,30 @@ var xmlTransactions = [
}
]
test('y-xml: Random tests (10)', async function randomXml10 (t) {
test('y-xml: Random tests (10)', async function xmlRandom10 (t) {
await applyRandomTests(t, xmlTransactions, 10)
})
test('y-xml: Random tests (42)', async function randomXml42 (t) {
test('y-xml: Random tests (42)', async function xmlRandom42 (t) {
await applyRandomTests(t, xmlTransactions, 42)
})
test('y-xml: Random tests (43)', async function randomXml43 (t) {
test('y-xml: Random tests (43)', async function xmlRandom43 (t) {
await applyRandomTests(t, xmlTransactions, 43)
})
test('y-xml: Random tests (44)', async function randomXml44 (t) {
test('y-xml: Random tests (44)', async function xmlRandom44 (t) {
await applyRandomTests(t, xmlTransactions, 44)
})
test('y-xml: Random tests (45)', async function randomXml45 (t) {
test('y-xml: Random tests (45)', async function xmlRandom45 (t) {
await applyRandomTests(t, xmlTransactions, 45)
})
test('y-xml: Random tests (46)', async function randomXml46 (t) {
test('y-xml: Random tests (46)', async function xmlRandom46 (t) {
await applyRandomTests(t, xmlTransactions, 46)
})
test('y-xml: Random tests (47)', async function randomXml47 (t) {
test('y-xml: Random tests (47)', async function xmlRandom47 (t) {
await applyRandomTests(t, xmlTransactions, 47)
})

View File

@@ -2,7 +2,7 @@
import _Y from '../../yjs/src/y.js'
import yArray from '../../y-array/src/y-array.js'
import yText from '../../y-text/src/Text.js'
import yText from '../../y-text/src/y-text.js'
import yMap from '../../y-map/src/y-map.js'
import yXml from '../../y-xml/src/y-xml.js'
import yTest from './test-connector.js'
@@ -48,11 +48,17 @@ export async function garbageCollectUsers (t, users) {
await Promise.all(users.map(u => u.db.emptyGarbageCollector()))
}
export function attrsToObject (attrs) {
export function attrsObject (dom) {
let keys = []
let yxml = dom.__yxml
for (let i = 0; i < dom.attributes.length; i++) {
keys.push(dom.attributes[i].name)
}
keys = yxml._domFilter(dom, keys)
let obj = {}
for (var i = 0; i < attrs.length; i++) {
let attr = attrs[i]
obj[attr.name] = attr.value
for (let i = 0; i < keys.length; i++) {
let key = keys[i]
obj[key] = dom.getAttribute(key)
}
return obj
}
@@ -61,8 +67,10 @@ export function domToJson (dom) {
if (dom.nodeType === document.TEXT_NODE) {
return dom.textContent
} else if (dom.nodeType === document.ELEMENT_NODE) {
let attributes = attrsToObject(dom.attributes)
let children = Array.from(dom.childNodes.values()).map(domToJson)
let attributes = attrsObject(dom, dom.__yxml)
let children = Array.from(dom.childNodes.values())
.filter(d => d.__yxml !== false)
.map(domToJson)
return {
name: dom.nodeName,
children: children,
@@ -198,6 +206,13 @@ export async function initArrays (t, opts) {
for (let name in share) {
result[name + i] = y.share[name]
}
y.share.xml.setDomFilter(function (d, attrs) {
if (d.nodeName === 'HIDDEN') {
return null
} else {
return attrs.filter(a => a !== 'hidden')
}
})
}
result.array0.delete(0, result.array0.length)
if (result.users[0].connector.testRoom != null) {

8
y.js

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +1,7 @@
/**
* yjs - A framework for real-time p2p shared editing on any data
* @version v13.0.0-17
* @version v13.0.0-21
* @license MIT
*/
@@ -292,6 +292,10 @@ class BinaryEncoder {
this.data = [];
}
get length () {
return this.data.length
}
get pos () {
return this.data.length
}
@@ -666,6 +670,11 @@ function extendConnector (Y/* :any */) {
if (opts.generateUserId !== false) {
this.setUserId(Y.utils.generateUserId());
}
if (opts.maxBufferLength == null) {
this.maxBufferLength = -1;
} else {
this.maxBufferLength = opts.maxBufferLength;
}
}
reconnect () {
@@ -821,14 +830,19 @@ function extendConnector (Y/* :any */) {
encoder.writeVarString(self.opts.room);
encoder.writeVarString('update');
let ops = self.broadcastOpBuffer;
self.broadcastOpBuffer = [];
let length = ops.length;
encoder.writeUint32(length);
for (var i = 0; i < length; i++) {
let encoderPosLen = encoder.pos;
encoder.writeUint32(0);
for (var i = 0; i < length && (self.maxBufferLength < 0 || encoder.length < self.maxBufferLength); i++) {
let op = ops[i];
Y.Struct[op.struct].binaryEncode(encoder, op);
}
encoder.setUint32(encoderPosLen, i);
self.broadcastOpBuffer = ops.slice(i);
self.broadcast(encoder.createBuffer());
if (i !== length) {
self.whenRemoteResponsive().then(broadcastOperations);
}
}
}
if (this.broadcastOpBuffer.length === 0) {
@@ -839,6 +853,20 @@ function extendConnector (Y/* :any */) {
}
}
/*
* Somehow check the responsiveness of the remote clients/server
* Default behavior:
* Wait 100ms before broadcasting the next batch of operations
*
* Only used when maxBufferLength is set
*
*/
whenRemoteResponsive () {
return new Promise(function (resolve) {
setTimeout(resolve, 100);
})
}
/*
You received a raw message, and you know that it is intended for Yjs. Then call this function.
*/
@@ -3434,6 +3462,51 @@ function Utils (Y) {
}
};
Y.utils.getRelativePosition = function (type, offset) {
if (type == null) {
return null
} else {
if (type._content.length <= offset) {
return ['endof', type._model[0], type._model[1]]
} else {
return type._content[offset].id
}
}
};
Y.utils.fromRelativePosition = function (y, id) {
var offset = 0;
var op;
if (id[0] === 'endof') {
id = y.db.os.find(id.slice(1)).end;
op = y.db.os.findNodeWithUpperBound(id).val;
if (!op.deleted) {
offset = op.content != null ? op.content.length : 1;
}
} else {
op = y.db.os.findNodeWithUpperBound(id).val;
if (!op.deleted) {
offset = id[1] - op.id[1];
}
}
var type = y.db.getType(op.parent);
if (type == null || y.db.os.find(op.parent).deleted) {
return null
}
while (op.left != null) {
op = y.db.os.findNodeWithUpperBound(op.left).val;
if (!op.deleted) {
offset += op.content != null ? op.content.length : 1;
}
}
return {
type: type,
offset: offset
}
};
class NamedEventHandler {
constructor () {
this._eventListener = {};
@@ -3452,7 +3525,11 @@ function Utils (Y) {
this._eventListener[name] = listener.filter(e => e !== f);
}
emit (name, value) {
(this._eventListener[name] || []).forEach(l => l(value));
let listener = this._eventListener[name] || [];
if (name === 'error' && listener.length === 0) {
console.error(value);
}
listener.forEach(l => l(value));
}
destroy () {
this._eventListener = null;
@@ -4793,9 +4870,13 @@ function extend (Y) {
}
transact (makeGen) {
const t = new Transaction(this);
while (makeGen != null) {
makeGen.call(t);
makeGen = this.getNextRequest();
try {
while (makeGen != null) {
makeGen.call(t);
makeGen = this.getNextRequest();
}
} catch (e) {
this.y.emit('error', e);
}
}
destroy () {

File diff suppressed because one or more lines are too long

1660
y.test.js

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long