Compare commits

..

20 Commits

Author SHA1 Message Date
Kevin Jahns
11a4271fd1 13.0.0-45 2018-01-10 00:18:50 +01:00
Kevin Jahns
c7670915c7 Merge branch 'master' of github.com:y-js/yjs 2018-01-10 00:17:34 +01:00
Kevin Jahns
eb2d596538 implement mutualExclude factory 2018-01-10 00:17:26 +01:00
Kevin Jahns
48e17ea1a7 13.0.0-44 2018-01-10 00:16:33 +01:00
Kevin Jahns
1a22fdd45e persistence improvements 2018-01-10 00:11:25 +01:00
Kevin Jahns
07cf0b3436 export AbstractPersistence 2018-01-08 17:30:30 +01:00
Kevin Jahns
5a68b9f4ad loaded event when loaded from persistence adapter 2018-01-08 02:28:46 +01:00
Kevin Jahns
445dd3e0da fix several y-xml bugs 2018-01-03 03:50:27 +01:00
Kevin Jahns
0ba97d78f8 better relative cursor positions for text editing - decrease number of generated messages for cursor 2017-12-31 16:14:02 +01:00
Kevin Jahns
fc5be5c7cc fix empty string insertion bug 2017-12-31 14:49:20 +01:00
Kevin Jahns
f2debc150c reimplement persistence approach 2017-12-24 03:18:00 +01:00
Kevin Jahns
08f37a86e3 13.0.0-43 2017-12-21 16:06:29 +01:00
Kevin Jahns
f5d17e6236 filter y-xml when domFilter is set 2017-12-21 16:05:50 +01:00
Kevin Jahns
8f3bd7170a 13.0.0-42 2017-12-19 17:39:01 +01:00
Kevin Jahns
5586334549 fix initial content in y-array 2017-12-19 17:37:04 +01:00
Kevin Jahns
24c1e4dcc8 13.0.0-41 2017-12-14 14:30:02 +01:00
Kevin Jahns
d61bbecf4e fix tree walker on YXmlFragment 2017-12-14 14:29:16 +01:00
Kevin Jahns
85492ad2e0 fix drawing example. Add drawing hook for y-xml 2017-12-13 12:49:34 +01:00
Kevin Jahns
02253f9a8d fix log outputs 2017-12-13 10:28:19 +01:00
Kevin Jahns
8105bef1af work on drawing demo 2017-12-06 19:20:52 -08:00
49 changed files with 1300 additions and 26653 deletions

View File

@@ -12,12 +12,8 @@
</style>
<button type="button" id="clearDrawingCanvas">Clear Drawing</button>
<svg id="drawingCanvas" viewbox="0 0 100 100" width="100%"></svg>
<script src="../../y.js"></script>
<script src="../../../y-array/y-array.js"></script>
<script src="../../../y-map/dist/y-map.js"></script>
<script src="../../../y-memory/y-memory.js"></script>
<script src="../../../y-websockets-client/y-websockets-client.js"></script>
<script src="../bower_components/d3/d3.js"></script>
<script src="../yjs-dist.js"></script>
<script src="../bower_components/d3/d3.min.js"></script>
<script src="./index.js"></script>
</body>
</html>

View File

@@ -1,84 +1,76 @@
/* globals Y, d3 */
'strict mode'
Y({
db: {
name: 'memory'
},
let y = new Y({
connector: {
name: 'websockets-client',
room: 'drawing-example',
url: 'localhost:1234'
},
sourceDir: '/bower_components',
share: {
drawing: 'Array'
}
}).then(function (y) {
window.yDrawing = y
var drawing = y.share.drawing
var renderPath = d3.svg.line()
.x(function (d) { return d[0] })
.y(function (d) { return d[1] })
.interpolate('basis')
var svg = d3.select('#drawingCanvas')
.call(d3.behavior.drag()
.on('dragstart', dragstart)
.on('drag', drag)
.on('dragend', dragend))
// create line from a shared array object and update the line when the array changes
function drawLine (yarray) {
var line = svg.append('path').datum(yarray.toArray())
line.attr('d', renderPath)
yarray.observe(function (event) {
// we only implement insert events that are appended to the end of the array
event.values.forEach(function (value) {
line.datum().push(value)
})
line.attr('d', renderPath)
})
}
// call drawLine every time an array is appended
y.share.drawing.observe(function (event) {
if (event.type === 'insert') {
event.values.forEach(drawLine)
} else {
// just remove all elements (thats what we do anyway)
svg.selectAll('path').remove()
}
})
// draw all existing content
for (var i = 0; i < drawing.length; i++) {
drawLine(drawing.get(i))
}
// clear canvas on request
document.querySelector('#clearDrawingCanvas').onclick = function () {
drawing.delete(0, drawing.length)
}
var sharedLine = null
function dragstart () {
drawing.insert(drawing.length, [Y.Array])
sharedLine = drawing.get(drawing.length - 1)
}
// After one dragged event is recognized, we ignore them for 33ms.
var ignoreDrag = null
function drag () {
if (sharedLine != null && ignoreDrag == null) {
ignoreDrag = window.setTimeout(function () {
ignoreDrag = null
}, 33)
sharedLine.push([d3.mouse(this)])
}
}
function dragend () {
sharedLine = null
window.clearTimeout(ignoreDrag)
ignoreDrag = null
url: 'http://127.0.0.1:1234',
room: 'drawing-example'
// maxBufferLength: 100
}
})
window.yDrawing = y
var drawing = y.define('drawing', Y.Array)
var renderPath = d3.svg.line()
.x(function (d) { return d[0] })
.y(function (d) { return d[1] })
.interpolate('basic')
var svg = d3.select('#drawingCanvas')
.call(d3.behavior.drag()
.on('dragstart', dragstart)
.on('drag', drag)
.on('dragend', dragend))
// create line from a shared array object and update the line when the array changes
function drawLine (yarray) {
var line = svg.append('path').datum(yarray.toArray())
line.attr('d', renderPath)
yarray.observe(function (event) {
line.remove()
line = svg.append('path').datum(yarray.toArray())
line.attr('d', renderPath)
})
}
// call drawLine every time an array is appended
drawing.observe(function (event) {
event.removedElements.forEach(function () {
// if one is deleted, all will be deleted!!
svg.selectAll('path').remove()
})
event.addedElements.forEach(function (path) {
drawLine(path)
})
})
// draw all existing content
for (var i = 0; i < drawing.length; i++) {
drawLine(drawing.get(i))
}
// clear canvas on request
document.querySelector('#clearDrawingCanvas').onclick = function () {
drawing.delete(0, drawing.length)
}
var sharedLine = null
function dragstart () {
drawing.insert(drawing.length, [Y.Array])
sharedLine = drawing.get(drawing.length - 1)
}
// After one dragged event is recognized, we ignore them for 33ms.
var ignoreDrag = null
function drag () {
if (sharedLine != null && ignoreDrag == null) {
ignoreDrag = window.setTimeout(function () {
ignoreDrag = null
}, 10)
sharedLine.push([d3.mouse(this)])
}
}
function dragend () {
sharedLine = null
window.clearTimeout(ignoreDrag)
ignoreDrag = null
}

View File

@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html>
</head>
<script src="../yjs-dist.js"></script>
<script src="../bower_components/d3/d3.min.js"></script>
<script src="./index.js"></script>
<style>
magic-drawing .drawingCanvas path {
fill: none;
stroke: blue;
stroke-width: 2px;
stroke-linejoin: round;
stroke-linecap: round;
}
magic-drawing .drawingCanvas {
width: 500px;
height: 500px;
cursor: default;
padding:1px;
border:1px solid #021a40;
}
magic-drawing .clearDrawingButton {
position: absolute;
top: 0;
left: 0;
}
magic-drawing {
position: relative;
display: block;
}
</style>
</head>
<body contenteditable="true">
</body>
</html>

View File

@@ -0,0 +1,134 @@
/* global Y, d3 */
window.onload = function () {
window.yXmlType.bindToDom(document.body)
}
window.addMagicDrawing = function addMagicDrawing () {
let mt = document.createElement('magic-drawing')
mt.dataset.yjsHook = 'magic-drawing'
document.body.append(mt)
}
var renderPath = d3.svg.line()
.x(function (d) { return d[0] })
.y(function (d) { return d[1] })
.interpolate('basic')
function initDrawingBindings (type, dom) {
dom.contentEditable = 'false'
dom.dataset.yjsHook = 'magic-drawing'
var drawing = type.get('drawing')
if (drawing === undefined) {
drawing = type.set('drawing', new Y.Array())
}
var canvas = dom.querySelector('.drawingCanvas')
if (canvas == null) {
canvas = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
canvas.setAttribute('class', 'drawingCanvas')
canvas.setAttribute('viewbox', '0 0 100 100')
dom.insertBefore(canvas, null)
}
var clearDrawingButton = dom.querySelector('.clearDrawingButton')
if (clearDrawingButton == null) {
clearDrawingButton = document.createElement('button')
clearDrawingButton.setAttribute('type', 'button')
clearDrawingButton.setAttribute('class', 'clearDrawingButton')
clearDrawingButton.innerText = 'Clear Drawing'
dom.insertBefore(clearDrawingButton, null)
}
var svg = d3.select(canvas)
.call(d3.behavior.drag()
.on('dragstart', dragstart)
.on('drag', drag)
.on('dragend', dragend))
// create line from a shared array object and update the line when the array changes
function drawLine (yarray, svg) {
var line = svg.append('path').datum(yarray.toArray())
line.attr('d', renderPath)
yarray.observe(function (event) {
line.remove()
line = svg.append('path').datum(yarray.toArray())
line.attr('d', renderPath)
})
}
// call drawLine every time an array is appended
drawing.observe(function (event) {
event.removedElements.forEach(function () {
// if one is deleted, all will be deleted!!
svg.selectAll('path').remove()
})
event.addedElements.forEach(function (path) {
drawLine(path, svg)
})
})
// draw all existing content
for (var i = 0; i < drawing.length; i++) {
drawLine(drawing.get(i), svg)
}
// clear canvas on request
clearDrawingButton.onclick = function () {
drawing.delete(0, drawing.length)
}
var sharedLine = null
function dragstart () {
drawing.insert(drawing.length, [Y.Array])
sharedLine = drawing.get(drawing.length - 1)
}
// After one dragged event is recognized, we ignore them for 33ms.
var ignoreDrag = null
function drag () {
if (sharedLine != null && ignoreDrag == null) {
ignoreDrag = window.setTimeout(function () {
ignoreDrag = null
}, 10)
sharedLine.push([d3.mouse(this)])
}
}
function dragend () {
sharedLine = null
window.clearTimeout(ignoreDrag)
ignoreDrag = null
}
}
Y.XmlHook.addHook('magic-drawing', {
fillType: function (dom, type) {
initDrawingBindings(type, dom)
},
createDom: function (type) {
const dom = document.createElement('magic-drawing')
initDrawingBindings(type, dom)
return dom
}
})
// initialize a shared object. This function call returns a promise!
let y = new Y({
connector: {
name: 'websockets-client',
url: 'http://127.0.0.1:1234',
room: 'html-editor-example6'
// maxBufferLength: 100
}
})
window.yXml = y
window.yXmlType = y.define('xml', Y.XmlFragment)
window.undoManager = new Y.utils.UndoManager(window.yXmlType, {
captureTimeout: 500
})
document.onkeydown = function interceptUndoRedo (e) {
if (e.keyCode === 90 && e.metaKey) {
if (!e.shiftKey) {
window.undoManager.undo()
} else {
window.undoManager.redo()
}
e.preventDefault()
}
}

View File

@@ -1,10 +1,7 @@
<!DOCTYPE html>
<html>
</head>
<!-- jquery is not required for y-xml. It is just here for convenience, and to test batch operations. -->
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<script src="../yjs-dist.js"></script>
<script src="./canvasjs.min.js"></script>
<script src="./index.js"></script>
</head>
<body contenteditable="true">

View File

@@ -1,77 +1,20 @@
/* global Y, HTMLElement, customElements, CanvasJS */
/* global Y */
window.onload = function () {
window.yXmlType.bindToDom(document.body)
let mt = document.createElement('magic-table')
mt.innerHTML = '<table><tr><th>Amount</th></tr><tr><td>1</td></tr><tr><td>1</td></tr></table>'
document.body.append(mt)
}
class MagicTable extends HTMLElement {
constructor () {
super()
this.createShadowRoot()
}
get _yjsHook () {
return 'magic-table'
}
showTable () {
this.shadowRoot.innerHTML = ''
this.shadowRoot.append(document.createElement('content'))
}
showDiagram () {
let dataPoints = []
this.querySelectorAll('td').forEach(td => {
let number = Number(td.textContent)
dataPoints.push({
x: (dataPoints.length + 1) * 10,
y: number,
label: '<magic-table> content'
})
})
this.shadowRoot.innerHTML = ''
var chart = new CanvasJS.Chart(this.shadowRoot,
{
title: {
text: 'Bar chart'
},
data: [
{
type: 'bar',
dataPoints: dataPoints
}
]
})
chart.render()
// this.shadowRoot.innerHTML = '<p>dtrn</p>'
}
}
customElements.define('magic-table', MagicTable)
Y.XmlHook.addHook('magic-table', {
fillType: function (dom, type) {
type.set('table', new Y.XmlElement(dom.querySelector('table')))
},
createDom: function (type) {
const table = type.get('table').getDom()
const dom = document.createElement('magic-table')
dom.insertBefore(table, null)
return dom
}
})
let persistence = null // new Y.IndexedDBPersistence()
// initialize a shared object. This function call returns a promise!
let y = new Y({
connector: {
name: 'websockets-client',
url: 'http://127.0.0.1:1234',
room: 'html-editor-example6'
room: 'x'
// maxBufferLength: 100
}
})
}, persistence)
window.yXml = y
window.yXmlType = y.define('xml', Y.XmlFragment)
window.undoManager = new Y.utils.UndoManager(window.yXmlType, {

File diff suppressed because it is too large Load Diff

View File

@@ -9,12 +9,15 @@
"author": "Kevin Jahns",
"license": "MIT",
"dependencies": {
"monaco-editor": "^0.8.3"
"monaco-editor": "^0.8.3",
"rollup": "^0.52.3"
},
"devDependencies": {
"standard": "^10.0.2"
},
"standard": {
"ignore": ["bower_components"]
"ignore": [
"bower_components"
]
}
}

View File

@@ -4,7 +4,7 @@ import commonjs from 'rollup-plugin-commonjs'
var pkg = require('./package.json')
export default {
input: 'yjs-dist.esm',
input: 'yjs-dist.mjs',
name: 'Y',
output: {
file: 'yjs-dist.js',

View File

@@ -1,7 +1,9 @@
import Y from '../src/Y.js'
import yWebsocketsClient from '../../y-websockets-client/src/y-websockets-client.js'
import IndexedDBPersistence from '../../y-indexeddb/src/y-indexeddb.js'
Y.extend(yWebsocketsClient)
Y.IndexedDBPersistence = IndexedDBPersistence
export default Y

1150
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "yjs",
"version": "13.0.0-40",
"version": "13.0.0-45",
"description": "A framework for real-time p2p shared editing on any data",
"main": "./y.node.js",
"browser": "./y.js",
@@ -65,9 +65,6 @@
"tag-dist-files": "^0.1.6"
},
"dependencies": {
"debug": "^2.6.8",
"fast-diff": "^1.1.2",
"utf-8": "^1.0.0",
"utf8": "^2.1.2"
"debug": "^2.6.8"
}
}

View File

@@ -3,7 +3,7 @@ import commonjs from 'rollup-plugin-commonjs'
import multiEntry from 'rollup-plugin-multi-entry'
export default {
input: 'test/y-xml.tests.js',
input: 'test/index.js',
name: 'y-tests',
sourcemap: true,
output: {
@@ -11,12 +11,12 @@ export default {
format: 'umd'
},
plugins: [
multiEntry(),
nodeResolve({
main: true,
module: true,
browser: true
}),
commonjs(),
multiEntry()
commonjs()
]
}

View File

@@ -1,4 +1,3 @@
import utf8 from 'utf-8'
import ID from '../Util/ID.js'
import { default as RootID, RootFakeUserID } from '../Util/RootID.js'
@@ -91,7 +90,8 @@ export default class BinaryDecoder {
for (let i = 0; i < len; i++) {
bytes[i] = this.uint8arr[this.pos++]
}
return utf8.getStringFromBytes(bytes)
let encodedString = String.fromCodePoint(...bytes)
return decodeURIComponent(escape(encodedString))
}
/**
* Look ahead and read varString without incrementing position

View File

@@ -1,4 +1,3 @@
import utf8 from 'utf-8'
import { RootFakeUserID } from '../Util/RootID.js'
const bits7 = 0b1111111
@@ -62,7 +61,8 @@ export default class BinaryEncoder {
}
writeVarString (str) {
let bytes = utf8.setBytesFromString(str)
let encodedString = unescape(encodeURIComponent(str))
let bytes = encodedString.split('').map(c => c.codePointAt())
let len = bytes.length
this.writeVarUint(len)
for (let i = 0; i < len; i++) {

View File

@@ -132,6 +132,7 @@ export default class AbstractConnector {
f()
}
this.whenSyncedListeners = []
this.y._setContentReady()
this.y.emit('synced')
}
}
@@ -268,7 +269,7 @@ export default class AbstractConnector {
if (messageType === 'sync step 2' && senderConn.auth === 'write') {
readSyncStep2(decoder, encoder, y, senderConn, sender)
} else if (messageType === 'update' && (skipAuth || senderConn.auth === 'write')) {
integrateRemoteStructs(decoder, encoder, y, senderConn, sender)
integrateRemoteStructs(y, decoder)
} else {
throw new Error('Unable to receive message')
}

View File

@@ -0,0 +1,16 @@
import { writeStructs } from './syncStep1.js'
import { integrateRemoteStructs } from './integrateRemoteStructs.js'
import { readDeleteSet, writeDeleteSet } from './deleteSet.js'
import BinaryEncoder from '../Binary/Encoder.js'
export function fromBinary (y, decoder) {
integrateRemoteStructs(y, decoder)
readDeleteSet(y, decoder)
}
export function toBinary (y) {
let encoder = new BinaryEncoder()
writeStructs(y, encoder, new Map())
writeDeleteSet(y, encoder)
return encoder
}

View File

@@ -65,7 +65,7 @@ export function stringifyStructs (y, decoder, strBuilder) {
}
}
export function integrateRemoteStructs (decoder, encoder, y) {
export function integrateRemoteStructs (y, decoder) {
const len = decoder.readUint32()
for (let i = 0; i < len; i++) {
let reference = decoder.readVarUint()

View File

@@ -30,7 +30,7 @@ export function sendSyncStep1 (connector, syncUser) {
connector.send(syncUser, encoder.createBuffer())
}
export default function writeStructs (encoder, decoder, y, ss) {
export function writeStructs (y, encoder, ss) {
const lenPos = encoder.pos
encoder.writeUint32(0)
let len = 0
@@ -60,7 +60,7 @@ export function readSyncStep1 (decoder, encoder, y, senderConn, sender) {
encoder.writeVarString('sync step 2')
encoder.writeVarString(y.connector.authInfo || '')
const ss = readStateSet(decoder)
writeStructs(encoder, decoder, y, ss)
writeStructs(y, encoder, ss)
writeDeleteSet(y, encoder)
y.connector.send(senderConn.uid, encoder.createBuffer())
senderConn.receivedSyncStep2 = true

View File

@@ -22,7 +22,7 @@ export function stringifySyncStep2 (y, decoder, strBuilder) {
}
export function readSyncStep2 (decoder, encoder, y, senderConn, sender) {
integrateRemoteStructs(decoder, encoder, y)
integrateRemoteStructs(y, decoder)
readDeleteSet(y, decoder)
y.connector._setSyncedWith(sender)
}

View File

@@ -1,47 +1,93 @@
// import BinaryEncoder from './Binary/Encoder.js'
import BinaryEncoder from './Binary/Encoder.js'
import BinaryDecoder from './Binary/Decoder.js'
import { toBinary, fromBinary } from './MessageHandler/binaryEncode.js'
import { integrateRemoteStructs } from './MessageHandler/integrateRemoteStructs.js'
import { createMutualExclude } from './Util/mutualExclude.js'
export default function extendPersistence (Y) {
class AbstractPersistence {
constructor (y, opts) {
this.y = y
this.opts = opts
this.saveOperationsBuffer = []
this.log = Y.debug('y:persistence')
}
function getFreshCnf () {
let buffer = new BinaryEncoder()
buffer.writeUint32(0)
return {
len: 0,
buffer
}
}
saveToMessageQueue (binary) {
this.log('Room %s: Save message to message queue', this.y.options.connector.room)
}
export default class AbstractPersistence {
constructor (opts) {
this.opts = opts
this.ys = new Map()
this.mutualExclude = createMutualExclude()
}
saveOperations (ops) {
ops = ops.map(function (op) {
return Y.Struct[op.struct].encode(op)
})
/*
const saveOperations = () => {
if (this.saveOperationsBuffer.length > 0) {
let encoder = new BinaryEncoder()
encoder.writeVarString(this.opts.room)
encoder.writeVarString('update')
let ops = this.saveOperationsBuffer
this.saveOperationsBuffer = []
let length = ops.length
encoder.writeUint32(length)
for (var i = 0; i < length; i++) {
let op = ops[i]
Y.Struct[op.struct].binaryEncode(encoder, op)
_init (y) {
let cnf = this.ys.get(y)
if (cnf === undefined) {
cnf = getFreshCnf()
this.ys.set(y, cnf)
this.init(y)
y.on('afterTransaction', (y, transaction) => {
let cnf = this.ys.get(y)
if (cnf.len > 0) {
cnf.buffer.setUint32(0, cnf.len)
this.saveUpdate(y, cnf.buffer.createBuffer(), transaction)
let _cnf = getFreshCnf()
for (let key in _cnf) {
cnf[key] = _cnf[key]
}
this.saveToMessageQueue(encoder.createBuffer())
}
}
*/
if (this.saveOperationsBuffer.length === 0) {
this.saveOperationsBuffer = ops
} else {
this.saveOperationsBuffer = this.saveOperationsBuffer.concat(ops)
}
})
}
return this.retrieve(y).then(function () {
return Promise.resolve(cnf)
})
}
deinit (y) {
this.ys.delete(y)
}
destroy () {
this.ys = null
}
/* overwrite */
saveUpdate (buffer) {
}
/**
* Save struct to update buffer.
* saveUpdate is called when transaction ends
*/
saveStruct (y, struct) {
let cnf = this.ys.get(y)
if (cnf !== undefined) {
this.mutualExclude(function () {
struct._toBinary(cnf.buffer)
cnf.len++
})
}
}
Y.AbstractPersistence = AbstractPersistence
/* overwrite */
retrieve (y, model, updates) {
this.mutualExclude(function () {
y.transact(function () {
if (model != null) {
fromBinary(y, new BinaryDecoder(new Uint8Array(model)))
y._setContentReady()
}
if (updates != null) {
for (let i = 0; i < updates.length; i++) {
integrateRemoteStructs(y, new BinaryDecoder(new Uint8Array(updates[i])))
y._setContentReady()
}
}
})
})
}
/* overwrite */
persist (y) {
return toBinary(y).createBuffer()
}
}

View File

@@ -7,7 +7,7 @@ import { logID } from '../MessageHandler/messageToString.js'
* TODO: implement getItemCleanStartNode for better performance (only one lookup)
*/
export function deleteItemRange (y, user, clock, range) {
const createDelete = y.connector._forwardAppliedStructs
const createDelete = y.connector !== null && y.connector._forwardAppliedStructs
let item = y.os.getItemCleanStart(new ID(user, clock))
if (item !== null) {
if (!item._deleted) {
@@ -70,12 +70,12 @@ export default class Delete {
// from remote
const id = this._targetID
deleteItemRange(y, id.user, id.clock, this._length)
} else {
} else if (y.connector !== null) {
// from local
y.connector.broadcastStruct(this)
}
if (y.persistence !== null) {
y.persistence.saveOperations(this)
y.persistence.saveStruct(y, this)
}
}
_logString () {

View File

@@ -87,16 +87,18 @@ export default class Item {
return this._right
}
_delete (y, createDelete = true) {
this._deleted = true
y.ds.markDeleted(this._id, this._length)
if (createDelete) {
let del = new Delete()
del._targetID = this._id
del._length = this._length
del._integrate(y, true)
if (!this._deleted) {
this._deleted = true
y.ds.markDeleted(this._id, this._length)
if (createDelete) {
let del = new Delete()
del._targetID = this._id
del._length = this._length
del._integrate(y, true)
}
transactionTypeChanged(y, this._parent, this._parentSub)
y._transaction.deletedStructs.add(this)
}
transactionTypeChanged(y, this._parent, this._parentSub)
y._transaction.deletedStructs.add(this)
}
/**
* This is called right before this struct receives any children.
@@ -216,11 +218,11 @@ export default class Item {
y.os.put(this)
transactionTypeChanged(y, parent, parentSub)
if (this._id.user !== RootFakeUserID) {
if (y.connector._forwardAppliedStructs || this._id.user === y.userID) {
if (y.connector !== null && (y.connector._forwardAppliedStructs || this._id.user === y.userID)) {
y.connector.broadcastStruct(this)
}
if (y.persistence !== null) {
y.persistence.saveOperations(this)
y.persistence.saveStruct(y, this)
}
}
}

View File

@@ -5,15 +5,38 @@ import { logID } from '../MessageHandler/messageToString.js'
import YEvent from '../Util/YEvent.js'
class YArrayEvent extends YEvent {
constructor (yarray, remote) {
constructor (yarray, remote, transaction) {
super(yarray)
this.remote = remote
this._transaction = transaction
}
get addedElements () {
const target = this.target
const transaction = this._transaction
const addedElements = new Set()
transaction.newTypes.forEach(function (type) {
if (type._parent === target && !transaction.deletedStructs.has(type)) {
addedElements.add(type)
}
})
return addedElements
}
get removedElements () {
const target = this.target
const transaction = this._transaction
const removedElements = new Set()
transaction.deletedStructs.forEach(function (struct) {
if (struct._parent === target && !transaction.newTypes.has(struct)) {
removedElements.add(struct)
}
})
return removedElements
}
}
export default class YArray extends Type {
_callObserver (transaction, parentSubs, remote) {
this._callEventHandler(transaction, new YArrayEvent(this, remote))
this._callEventHandler(transaction, new YArrayEvent(this, remote, transaction))
}
get (pos) {
let n = this._start
@@ -184,8 +207,12 @@ export default class YArray extends Type {
prevJsonIns._content.push(c)
}
}
if (prevJsonIns !== null && y !== null) {
prevJsonIns._integrate(y)
if (prevJsonIns !== null) {
if (y !== null) {
prevJsonIns._integrate(y)
} else if (prevJsonIns._left === null) {
this._start = prevJsonIns
}
}
})
}
@@ -214,6 +241,17 @@ export default class YArray extends Type {
}
this.insertAfter(left, content)
}
push (content) {
let n = this._start
let lastUndeleted = null
while (n !== null) {
if (!n._deleted) {
lastUndeleted = n
}
n = n._right
}
this.insertAfter(lastUndeleted, content)
}
_logString () {
const left = this._left !== null ? this._left._lastId : null
const origin = this._origin !== null ? this._origin._lastId : null

View File

@@ -56,7 +56,7 @@ export default class YMap extends Type {
this._transact(y => {
const old = this._map.get(key) || null
if (old !== null) {
if (old instanceof ItemJSON && old._content[0] === value) {
if (old.constructor === ItemJSON && !old._deleted && old._content[0] === value) {
// Trying to overwrite with same value
// break here
return value

View File

@@ -24,6 +24,9 @@ export default class YText extends YArray {
return strBuilder.join('')
}
insert (pos, text) {
if (text.length <= 0) {
return
}
this._transact(y => {
let left = null
let right = this._start

View File

@@ -1,4 +1,3 @@
// import diff from 'fast-diff'
import { defaultDomFilter } from './utils.js'
import YMap from '../YMap.js'
@@ -48,6 +47,11 @@ export default class YXmlElement extends YXmlFragment {
return dom
}
}
_bindToDom (dom, _document) {
_document = _document || document
this._dom = dom
dom._yxml = this
}
_fromBinary (y, decoder) {
const missing = super._fromBinary(y, decoder)
this.nodeName = decoder.readVarString()

View File

@@ -7,7 +7,7 @@ import YArray from '../YArray.js'
import YXmlEvent from './YXmlEvent.js'
import { YXmlText, YXmlHook } from './y-xml'
import { logID } from '../../MessageHandler/messageToString.js'
import diff from 'fast-diff'
import diff from '../../Util/simpleDiff.js'
function domToYXml (parent, doms, _document) {
const types = []
@@ -55,7 +55,7 @@ class YXmlTreeWalker {
}
}
do {
if (!n._deleted && n.constructor === YXmlFragment._YXmlElement && n._start !== null) {
if (!n._deleted && (n.constructor === YXmlFragment._YXmlElement || n.constructor === YXmlFragment) && n._start !== null) {
// walk down in the tree
n = n._start
} else {
@@ -101,9 +101,11 @@ export default class YXmlFragment extends YArray {
} catch (e) {
console.error(e)
}
/*
if (this._domObserver !== null) {
this._domObserver.takeRecords()
}
*/
token = true
}
}
@@ -143,6 +145,23 @@ export default class YXmlFragment extends YArray {
}
setDomFilter (f) {
this._domFilter = f
let attributes = new Map()
if (this.getAttributes !== undefined) {
let attrs = this.getAttributes()
for (let key in attrs) {
attributes.set(key, attrs[key])
}
}
let result = this._domFilter(this.nodeName, new Map(attributes))
if (result === null) {
this._delete(this._y)
} else {
attributes.forEach((value, key) => {
if (!result.has(key)) {
this.removeAttribute(key)
}
})
}
this.forEach(xml => {
xml.setDomFilter(f)
})
@@ -166,6 +185,9 @@ export default class YXmlFragment extends YArray {
this._dom._yxml = null
this._dom = null
}
if (this._beforeTransactionHandler !== undefined) {
this._y.off('beforeTransaction', this._beforeTransactionHandler)
}
}
insertDomElementsAfter (prev, doms, _document) {
const types = domToYXml(this, doms, _document)
@@ -199,9 +221,7 @@ export default class YXmlFragment extends YArray {
_document = _document || document
this._dom = dom
dom._yxml = this
// TODO: refine this..
if ((this.constructor !== YXmlFragment && this._parent !== this._y) || this._parent === null) {
// TODO: only top level YXmlFragment can bind. Also allow YXmlElements..
if (this._parent === null) {
return
}
this._y.on('beforeTransaction', beforeTransactionSelectionFixer)
@@ -258,9 +278,10 @@ export default class YXmlFragment extends YArray {
})
// Apply Dom changes on Y.Xml
if (typeof MutationObserver !== 'undefined') {
this._y.on('beforeTransaction', () => {
this._beforeTransactionHandler = () => {
this._domObserverListener(this._domObserver.takeRecords())
})
}
this._y.on('beforeTransaction', this._beforeTransactionHandler)
this._domObserverListener = mutations => {
this._mutualExclude(() => {
this._y.transact(() => {
@@ -274,19 +295,9 @@ export default class YXmlFragment extends YArray {
}
switch (mutation.type) {
case 'characterData':
var diffs = diff(yxml.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
yxml.delete(pos, d[1].length)
} else { // INSERT
yxml.insert(pos, d[1])
pos += d[1].length
}
}
var change = diff(yxml.toString(), dom.nodeValue)
yxml.delete(change.pos, change.remove)
yxml.insert(change.pos, change.insert)
break
case 'attributes':
if (yxml.constructor === YXmlFragment) {
@@ -313,6 +324,9 @@ export default class YXmlFragment extends YArray {
}
})
for (let dom of diffChildren) {
if (dom.yOnChildrenChanged !== undefined) {
dom.yOnChildrenChanged()
}
if (dom._yxml != null && dom._yxml !== false) {
applyChangesFromDom(dom)
}

View File

@@ -24,6 +24,11 @@ export default class YXmlHook extends YMap {
}
return this._dom
}
_unbindFromDom () {
this._dom._yxml = null
this._yxml = null
// TODO: cleanup hook?
}
_fromBinary (y, decoder) {
const missing = super._fromBinary(y, decoder)
this.hookName = decoder.readVarString()

View File

@@ -174,7 +174,7 @@ export function reflectChangesOnDom (events, _document) {
// let anchorViewPosition = getAnchorViewPosition(yxml._scrollElement)
if (yxml.constructor === YXmlText) {
yxml._dom.nodeValue = yxml.toString()
} else {
} else if (event.attributesChanged !== undefined) {
// update attributes
event.attributesChanged.forEach(attributeName => {
const value = yxml.getAttribute(attributeName)

View File

@@ -11,6 +11,10 @@ export default class ID {
return id !== null && id.user === this.user && id.clock === this.clock
}
lessThan (id) {
return this.user < id.user || (this.user === id.user && this.clock < id.clock)
if (id.constructor === ID) {
return this.user < id.user || (this.user === id.user && this.clock < id.clock)
} else {
return false
}
}
}

View File

@@ -27,7 +27,8 @@ export default class NamedEventHandler {
}
const listener = this._eventListener.get(name)
if (listener !== undefined) {
listener.remove(f)
listener.on.delete(f)
listener.once.delete(f)
}
}
emit (name, ...args) {

View File

@@ -12,6 +12,10 @@ export default class RootID {
return id !== null && id.user === this.user && id.name === this.name && id.type === this.type
}
lessThan (id) {
return this.user < id.user || (this.user === id.user && (this.name < id.name || (this.name === id.name && this.type < id.type)))
if (id.constructor === RootID) {
return this.user < id.user || (this.user === id.user && (this.name < id.name || (this.name === id.name && this.type < id.type)))
} else {
return true
}
}
}

15
src/Util/mutualExclude.js Normal file
View File

@@ -0,0 +1,15 @@
export function createMutualExclude () {
var token = true
return function mutualExclude (f) {
if (token) {
token = false
try {
f()
} catch (e) {
console.error(e)
}
token = true
}
}
}

View File

@@ -2,37 +2,31 @@ import ID from './ID.js'
import RootID from './RootID.js'
export function getRelativePosition (type, offset) {
if (offset === 0) {
return ['startof', type._id.user, type._id.clock || null, type._id.name || null, type._id.type || null]
} else {
let t = type._start
while (t !== null) {
if (t._deleted === false) {
if (t._length >= offset) {
return [t._id.user, t._id.clock + offset - 1]
}
if (t._right === null) {
return [t._id.user, t._id.clock + t._length - 1]
}
offset -= t._length
let t = type._start
while (t !== null) {
if (t._deleted === false) {
if (t._length > offset) {
return [t._id.user, t._id.clock + offset]
}
t = t._right
offset -= t._length
}
return null
t = t._right
}
return ['endof', type._id.user, type._id.clock || null, type._id.name || null, type._id.type || null]
}
export function fromRelativePosition (y, rpos) {
if (rpos[0] === 'startof') {
if (rpos[0] === 'endof') {
let id
if (rpos[3] === null) {
id = new ID(rpos[1], rpos[2])
} else {
id = new RootID(rpos[3], rpos[4])
}
const type = y.os.get(id)
return {
type: y.os.get(id),
offset: 0
type,
offset: type.length
}
} else {
let offset = 0
@@ -42,7 +36,7 @@ export function fromRelativePosition (y, rpos) {
return null
}
if (!struct._deleted) {
offset = rpos[1] - struct._id.clock + 1
offset = rpos[1] - struct._id.clock
}
struct = struct._left
while (struct !== null) {

19
src/Util/simpleDiff.js Normal file
View File

@@ -0,0 +1,19 @@
export default function simpleDiff (a, b) {
let left = 0 // number of same characters counting from left
let right = 0 // number of same characters counting from right
while (left < a.length && left < b.length && a[left] === b[left]) {
left++
}
if (left !== a.length || left !== b.length) {
// Only check right if a !== b
while (right + left < a.length && right + left < b.length && a[a.length - right - 1] === b[b.length - right - 1]) {
right++
}
}
return {
pos: left,
remove: a.length - left - right,
insert: b.slice(left, b.length - right)
}
}

View File

@@ -22,25 +22,44 @@ import debug from 'debug'
import Transaction from './Transaction.js'
export default class Y extends NamedEventHandler {
constructor (opts) {
constructor (room, opts, persistence) {
super()
this.room = room
if (opts != null) {
opts.connector.room = room
}
this._contentReady = false
this._opts = opts
this.userID = opts._userID != null ? opts._userID : generateUserID()
this.userID = generateUserID()
this.share = {}
this.ds = new DeleteStore(this)
this.os = new OperationStore(this)
this.ss = new StateStore(this)
this.connector = new Y[opts.connector.name](this, opts.connector)
if (opts.persistence != null) {
this.persistence = new Y[opts.persistence.name](this, opts.persistence)
this.persistence.retrieveContent()
} else {
this.persistence = null
}
this.connected = true
this._missingStructs = new Map()
this._readyToIntegrate = []
this._transaction = null
this.connector = null
this.connected = false
let initConnection = () => {
if (opts != null) {
this.connector = new Y[opts.connector.name](this, opts.connector)
this.connected = true
this.emit('connectorReady')
}
}
if (persistence != null) {
this.persistence = persistence
persistence._init(this).then(initConnection)
} else {
this.persistence = null
initConnection()
}
}
_setContentReady () {
if (!this._contentReady) {
this._contentReady = true
this.emit('content')
}
}
_beforeChange () {}
transact (f, remote = false) {
@@ -90,9 +109,6 @@ export default class Y extends NamedEventHandler {
set _start (start) {
return null
}
get room () {
return this._opts.connector.room
}
define (name, TypeConstructor) {
let id = new RootID(name, TypeConstructor)
let type = this.os.get(id)
@@ -123,11 +139,18 @@ export default class Y extends NamedEventHandler {
}
}
destroy () {
super.destroy()
this.share = null
if (this.connector.destroy != null) {
this.connector.destroy()
} else {
this.connector.disconnect()
if (this.connector != null) {
if (this.connector.destroy != null) {
this.connector.destroy()
} else {
this.connector.disconnect()
}
}
if (this.persistence !== null) {
this.persistence.deinit(this)
this.persistence = null
}
this.os = null
this.ds = null
@@ -155,7 +178,7 @@ Y.extend = function extendYjs () {
// TODO: The following assignments should be moved to yjs-dist
Y.AbstractConnector = Connector
Y.Persisence = Persistence
Y.AbstractPersistence = Persistence
Y.Array = YArray
Y.Map = YMap
Y.Text = YText

29
test/diff.tests.js Normal file
View File

@@ -0,0 +1,29 @@
import { test } from '../node_modules/cutest/cutest.mjs'
import simpleDiff from '../src/Util/simpleDiff.js'
import Chance from 'chance'
function runDiffTest (t, a, b, expected) {
let result = simpleDiff(a, b)
t.compare(result, expected, `Compare "${a}" with "${b}"`)
}
test('diff tests', async function diff1 (t) {
runDiffTest(t, 'abc', 'axc', { pos: 1, remove: 1, insert: 'x' })
runDiffTest(t, 'bc', 'xc', { pos: 0, remove: 1, insert: 'x' })
runDiffTest(t, 'ab', 'ax', { pos: 1, remove: 1, insert: 'x' })
runDiffTest(t, 'b', 'x', { pos: 0, remove: 1, insert: 'x' })
runDiffTest(t, '', 'abc', { pos: 0, remove: 0, insert: 'abc' })
runDiffTest(t, 'abc', 'xyz', { pos: 0, remove: 3, insert: 'xyz' })
runDiffTest(t, 'axz', 'au', { pos: 1, remove: 2, insert: 'u' })
runDiffTest(t, 'ax', 'axy', { pos: 2, remove: 0, insert: 'y' })
})
test('random diff tests', async function randomDiff (t) {
const chance = new Chance(t.getSeed() * 1000000000)
let a = chance.word()
let b = chance.word()
let change = simpleDiff(a, b)
let arr = Array.from(a)
arr.splice(change.pos, change.remove, ...Array.from(change.insert))
t.assert(arr.join('') === b, 'Applying change information is correct')
})

View File

@@ -54,6 +54,9 @@ test('varString', async function varString (t) {
testEncoding(t, writeVarString, readVarString, 'test!')
testEncoding(t, writeVarString, readVarString, '☺☺☺')
testEncoding(t, writeVarString, readVarString, '1234')
testEncoding(t, writeVarString, readVarString, '쾟')
testEncoding(t, writeVarString, readVarString, '龟') // surrogate length 3
testEncoding(t, writeVarString, readVarString, '😝') // surrogate length 4
})
test('varString random', async function varStringRandom (t) {

View File

@@ -3,6 +3,6 @@
<head>
</head>
<body>
<script type="module" src="./encode-decode.js"></script>
<script type="module" src="./index.js"></script>
</body>
</html>

6
test/index.js Normal file
View File

@@ -0,0 +1,6 @@
import './red-black-tree.js'
import './y-array.tests.js'
import './y-map.tests.js'
import './y-xml.tests.js'
import './encode-decode.tests.js'
import './diff.tests.js'

View File

@@ -280,7 +280,6 @@ test('deep element insert', async function xml16 (t) {
deepElement.append(boldElement)
deepElement.append(attrElement)
dom0.append(deepElement)
console.log(dom0.outerHTML)
let str0 = dom0.outerHTML
await flushAll(t, users)
let str1 = dom1.outerHTML
@@ -340,7 +339,6 @@ test('Filtering remote changes', async function xmlFilteringRemote (t) {
// check dom
paragraph.getDom().setAttribute('malicious', 'true')
span.getDom().setAttribute('malicious', 'true')
console.log(xml0.toString())
// check incoming attributes
xml1.get(0).get(0).setAttribute('malicious', 'true')
xml1.insert(0, [new Y.XmlElement('hideMe')])

View File

@@ -145,7 +145,7 @@ export async function initArrays (t, opts) {
} else {
connOpts = Object.assign({ role: 'slave' }, conn)
}
let y = new Y({
let y = new Y(connOpts.room, {
_userID: i, // evil hackery, don't try this at home
connector: connOpts
})

9
y.js

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

5717
y.node.js

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

19505
y.test.js

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long