Compare commits
28 Commits
v13.0.0-36
...
v13.0.0-45
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11a4271fd1 | ||
|
|
c7670915c7 | ||
|
|
eb2d596538 | ||
|
|
48e17ea1a7 | ||
|
|
1a22fdd45e | ||
|
|
07cf0b3436 | ||
|
|
5a68b9f4ad | ||
|
|
445dd3e0da | ||
|
|
0ba97d78f8 | ||
|
|
fc5be5c7cc | ||
|
|
f2debc150c | ||
|
|
08f37a86e3 | ||
|
|
f5d17e6236 | ||
|
|
8f3bd7170a | ||
|
|
5586334549 | ||
|
|
24c1e4dcc8 | ||
|
|
d61bbecf4e | ||
|
|
85492ad2e0 | ||
|
|
02253f9a8d | ||
|
|
8105bef1af | ||
|
|
4efa16e2dd | ||
|
|
ad44f59def | ||
|
|
9c471ea24d | ||
|
|
d9e76014f5 | ||
|
|
4091b7d004 | ||
|
|
dfc183643d | ||
|
|
cf8698f2b6 | ||
|
|
3595f14da7 |
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
35
examples/html-editor-drawing-hook/index.html
Normal file
35
examples/html-editor-drawing-hook/index.html
Normal 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>
|
||||
134
examples/html-editor-drawing-hook/index.js
Normal file
134
examples/html-editor-drawing-hook/index.js
Normal 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()
|
||||
}
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -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, {
|
||||
|
||||
663
examples/package-lock.json
generated
663
examples/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
1150
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "yjs",
|
||||
"version": "13.0.0-36",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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++) {
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
16
src/MessageHandler/binaryEncode.js
Normal file
16
src/MessageHandler/binaryEncode.js
Normal 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
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -24,19 +24,25 @@ 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
|
||||
let count = 0
|
||||
while (right !== null) {
|
||||
if (count <= pos && pos < count + right._length) {
|
||||
const rightLen = right._deleted ? 0 : (right._length - 1)
|
||||
if (count <= pos && pos <= count + rightLen) {
|
||||
const splitDiff = pos - count
|
||||
right = right._splitAt(this._y, pos - count)
|
||||
right = right._splitAt(this._y, splitDiff)
|
||||
left = right._left
|
||||
count += splitDiff
|
||||
break
|
||||
}
|
||||
count += right._length
|
||||
if (!right._deleted) {
|
||||
count += right._length
|
||||
}
|
||||
left = right
|
||||
right = right._right
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 = []
|
||||
@@ -17,7 +17,7 @@ function domToYXml (parent, doms, _document) {
|
||||
}
|
||||
if (parent._domFilter(d.nodeName, new Map()) !== null) {
|
||||
let type
|
||||
const hookName = d._yjsHook
|
||||
const hookName = d._yjsHook || (d.dataset != null ? d.dataset.yjsHook : undefined)
|
||||
if (hookName !== undefined) {
|
||||
type = new YXmlHook(hookName, d)
|
||||
} else if (d.nodeType === d.TEXT_NODE) {
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -68,7 +68,6 @@ export function afterTransactionSelectionFixer (y, transaction, remote) {
|
||||
}
|
||||
}
|
||||
if (shouldUpdate) {
|
||||
console.info('updating selection!!')
|
||||
browserSelection.setBaseAndExtent(
|
||||
anchorNode,
|
||||
anchorOffset,
|
||||
@@ -76,7 +75,4 @@ export function afterTransactionSelectionFixer (y, transaction, remote) {
|
||||
focusOffset
|
||||
)
|
||||
}
|
||||
// delete, so the objects can be gc'd
|
||||
relativeSelection = null
|
||||
browserSelection = null
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -193,19 +193,27 @@ export function reflectChangesOnDom (events, _document) {
|
||||
* only in the attributes (above)
|
||||
*/
|
||||
if (event.childListChanged && yxml.constructor !== YXmlHook) {
|
||||
// create fragment of undeleted nodes
|
||||
const fragment = _document.createDocumentFragment()
|
||||
let currentChild = dom.firstChild
|
||||
yxml.forEach(function (t) {
|
||||
fragment.appendChild(t.getDom(_document))
|
||||
let expectedChild = t.getDom(_document)
|
||||
if (expectedChild.parentNode === dom) {
|
||||
// is already attached to the dom. Look for it
|
||||
while (currentChild !== expectedChild) {
|
||||
let del = currentChild
|
||||
currentChild = currentChild.nextSibling
|
||||
dom.removeChild(del)
|
||||
}
|
||||
currentChild = currentChild.nextSibling
|
||||
} else {
|
||||
// this dom is not yet attached to dom
|
||||
dom.insertBefore(expectedChild, currentChild)
|
||||
}
|
||||
})
|
||||
// remove remainding nodes
|
||||
let lastChild = dom.lastChild
|
||||
while (lastChild !== null) {
|
||||
dom.removeChild(lastChild)
|
||||
lastChild = dom.lastChild
|
||||
while (currentChild !== null) {
|
||||
let tmp = currentChild.nextSibling
|
||||
dom.removeChild(currentChild)
|
||||
currentChild = tmp
|
||||
}
|
||||
// insert fragment of undeleted nodes
|
||||
dom.appendChild(fragment)
|
||||
}
|
||||
}
|
||||
/* TODO: smartscrolling
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
15
src/Util/mutualExclude.js
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
19
src/Util/simpleDiff.js
Normal 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)
|
||||
}
|
||||
}
|
||||
59
src/Y.js
59
src/Y.js
@@ -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
29
test/diff.tests.js
Normal 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')
|
||||
})
|
||||
@@ -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) {
|
||||
@@ -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
6
test/index.js
Normal 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'
|
||||
@@ -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')])
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user