Compare commits
1 Commits
ydb-integr
...
v13.0.0-29
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4f06e8f62 |
12
.babelrc
Normal file
12
.babelrc
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"presets": [
|
||||||
|
["latest", {
|
||||||
|
"es2015": {
|
||||||
|
"modules": false
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
"external-helpers"
|
||||||
|
]
|
||||||
|
}
|
||||||
10
.esdoc.json
10
.esdoc.json
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"source": "./src",
|
|
||||||
"destination": "./docs",
|
|
||||||
"plugins": [{
|
|
||||||
"name": "esdoc-standard-plugin",
|
|
||||||
"option": {
|
|
||||||
"accessor": {"access": ["public"], "autoPrivate": true}
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
14
.flowconfig
Normal file
14
.flowconfig
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[ignore]
|
||||||
|
.*/node_modules/.*
|
||||||
|
.*/dist/.*
|
||||||
|
.*/build/.*
|
||||||
|
|
||||||
|
[include]
|
||||||
|
./src/
|
||||||
|
./tests-lib/
|
||||||
|
./test/
|
||||||
|
|
||||||
|
[libs]
|
||||||
|
./declarations/
|
||||||
|
|
||||||
|
[options]
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,8 +1,4 @@
|
|||||||
node_modules
|
node_modules
|
||||||
bower_components
|
bower_components
|
||||||
docs
|
|
||||||
/y.*
|
/y.*
|
||||||
/examples/yjs-dist.js*
|
/examples/yjs-dist.js*
|
||||||
.vscode
|
|
||||||
.yjsPersisted
|
|
||||||
build
|
|
||||||
16
README.md
16
README.md
@@ -64,18 +64,6 @@ missing modules.
|
|||||||
<script src="./bower_components/yjs/y.js"></script>
|
<script src="./bower_components/yjs/y.js"></script>
|
||||||
```
|
```
|
||||||
|
|
||||||
### CDN
|
|
||||||
```
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/yjs@12/src/y.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/y-array@10/dist/y-array.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/y-websockets-client@8/dist/y-websockets-client.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/y-memory@8/dist/y-memory.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/y-map@10/dist/y-map.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/y-text@9/dist/y-text.js"></script>
|
|
||||||
// ..
|
|
||||||
// do the same for all modules you want to use
|
|
||||||
```
|
|
||||||
|
|
||||||
### Npm
|
### Npm
|
||||||
```
|
```
|
||||||
npm install --save yjs % add all y-* modules you want to use
|
npm install --save yjs % add all y-* modules you want to use
|
||||||
@@ -88,6 +76,7 @@ var Y = require('yjs')
|
|||||||
require('y-array')(Y) // add the y-array type to Yjs
|
require('y-array')(Y) // add the y-array type to Yjs
|
||||||
require('y-websockets-client')(Y)
|
require('y-websockets-client')(Y)
|
||||||
require('y-memory')(Y)
|
require('y-memory')(Y)
|
||||||
|
require('y-array')(Y)
|
||||||
require('y-map')(Y)
|
require('y-map')(Y)
|
||||||
require('y-text')(Y)
|
require('y-text')(Y)
|
||||||
// ..
|
// ..
|
||||||
@@ -100,6 +89,7 @@ import Y from 'yjs'
|
|||||||
import yArray from 'y-array'
|
import yArray from 'y-array'
|
||||||
import yWebsocketsClient from 'y-webrtc'
|
import yWebsocketsClient from 'y-webrtc'
|
||||||
import yMemory from 'y-memory'
|
import yMemory from 'y-memory'
|
||||||
|
import yArray from 'y-array'
|
||||||
import yMap from 'y-map'
|
import yMap from 'y-map'
|
||||||
import yText from 'y-text'
|
import yText from 'y-text'
|
||||||
// ..
|
// ..
|
||||||
@@ -248,7 +238,7 @@ The promise returns an instance of Y. We denote it with a lower case `y`.
|
|||||||
* y-websockets-client aways waits to sync with the server
|
* y-websockets-client aways waits to sync with the server
|
||||||
* y.connector.disconnect()
|
* y.connector.disconnect()
|
||||||
* Force to disconnect this instance from the other instances
|
* Force to disconnect this instance from the other instances
|
||||||
* y.connector.connect()
|
* y.connector.reconnect()
|
||||||
* Try to reconnect to the other instances (needs to be supported by the
|
* Try to reconnect to the other instances (needs to be supported by the
|
||||||
connector)
|
connector)
|
||||||
* Not supported by y-xmpp
|
* Not supported by y-xmpp
|
||||||
|
|||||||
@@ -24,8 +24,7 @@
|
|||||||
<body>
|
<body>
|
||||||
|
|
||||||
<div id="aceContainer"></div>
|
<div id="aceContainer"></div>
|
||||||
<script src="../../y.js"></script>
|
<script src="../bower_components/yjs/y.js"></script>
|
||||||
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
|
|
||||||
<script src="../bower_components/ace-builds/src/ace.js"></script>
|
<script src="../bower_components/ace-builds/src/ace.js"></script>
|
||||||
|
|
||||||
<script src="./index.js"></script>
|
<script src="./index.js"></script>
|
||||||
|
|||||||
@@ -1,17 +1,24 @@
|
|||||||
/* global Y, ace */
|
/* global Y, ace */
|
||||||
|
|
||||||
let y = new Y('ace-example', {
|
Y({
|
||||||
|
db: {
|
||||||
|
name: 'memory'
|
||||||
|
},
|
||||||
connector: {
|
connector: {
|
||||||
name: 'websockets-client',
|
name: 'websockets-client',
|
||||||
url: 'http://127.0.0.1:1234'
|
room: 'ace-example'
|
||||||
|
},
|
||||||
|
sourceDir: '/bower_components',
|
||||||
|
share: {
|
||||||
|
ace: 'Text' // y.share.textarea is of type Y.Text
|
||||||
}
|
}
|
||||||
|
}).then(function (y) {
|
||||||
|
window.yAce = y
|
||||||
|
|
||||||
|
// bind the textarea to a shared text element
|
||||||
|
var editor = ace.edit('aceContainer')
|
||||||
|
editor.setTheme('ace/theme/chrome')
|
||||||
|
editor.getSession().setMode('ace/mode/javascript')
|
||||||
|
|
||||||
|
y.share.ace.bindAce(editor)
|
||||||
})
|
})
|
||||||
|
|
||||||
window.yAce = y
|
|
||||||
|
|
||||||
// bind the textarea to a shared text element
|
|
||||||
var editor = ace.edit('aceContainer')
|
|
||||||
editor.setTheme('ace/theme/chrome')
|
|
||||||
editor.getSession().setMode('ace/mode/javascript')
|
|
||||||
|
|
||||||
y.define('ace', Y.Text).bindAce(editor)
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<input type="submit" value="Send">
|
<input type="submit" value="Send">
|
||||||
</form>
|
</form>
|
||||||
<script src="../../y.js"></script>
|
<script src="../../y.js"></script>
|
||||||
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
|
<script src="../../../y-websockets-client/dist/y-websockets-client.js"></script>
|
||||||
<script src="./index.js"></script>
|
<script src="./index.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
/* global Y */
|
/* global Y */
|
||||||
|
|
||||||
let y = new Y('chat-example', {
|
// initialize a shared object. This function call returns a promise!
|
||||||
|
var y = new Y({
|
||||||
connector: {
|
connector: {
|
||||||
name: 'websockets-client',
|
name: 'websockets-client',
|
||||||
url: 'http://127.0.0.1:1234'
|
room: 'chat-example'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -22,7 +23,6 @@ function appendMessage (message, position) {
|
|||||||
p.appendChild(document.createTextNode(message.message))
|
p.appendChild(document.createTextNode(message.message))
|
||||||
chatcontainer.insertBefore(p, chatcontainer.children[position] || null)
|
chatcontainer.insertBefore(p, chatcontainer.children[position] || null)
|
||||||
}
|
}
|
||||||
|
|
||||||
// This function makes sure that only 7 messages exist in the chat history.
|
// This function makes sure that only 7 messages exist in the chat history.
|
||||||
// The rest is deleted
|
// The rest is deleted
|
||||||
function cleanupChat () {
|
function cleanupChat () {
|
||||||
@@ -30,17 +30,23 @@ function cleanupChat () {
|
|||||||
chatprotocol.delete(0, chatprotocol.length - 7)
|
chatprotocol.delete(0, chatprotocol.length - 7)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cleanupChat()
|
|
||||||
|
|
||||||
// Insert the initial content
|
// Insert the initial content
|
||||||
chatprotocol.toArray().forEach(appendMessage)
|
chatprotocol.toArray().forEach(appendMessage)
|
||||||
|
cleanupChat()
|
||||||
|
|
||||||
// whenever content changes, make sure to reflect the changes in the DOM
|
// whenever content changes, make sure to reflect the changes in the DOM
|
||||||
chatprotocol.observe(function (event) {
|
chatprotocol.observe(function (event) {
|
||||||
|
if (event.type === 'insert') {
|
||||||
|
for (let i = 0; i < event.length; i++) {
|
||||||
|
appendMessage(event.values[i], event.index + i)
|
||||||
|
}
|
||||||
|
} else if (event.type === 'delete') {
|
||||||
|
for (let i = 0; i < event.length; i++) {
|
||||||
|
chatcontainer.children[event.index].remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
// concurrent insertions may result in a history > 7, so cleanup here
|
// concurrent insertions may result in a history > 7, so cleanup here
|
||||||
cleanupChat()
|
cleanupChat()
|
||||||
chatcontainer.innerHTML = ''
|
|
||||||
chatprotocol.toArray().forEach(appendMessage)
|
|
||||||
})
|
})
|
||||||
document.querySelector('#chatform').onsubmit = function (event) {
|
document.querySelector('#chatform').onsubmit = function (event) {
|
||||||
// the form is submitted
|
// the form is submitted
|
||||||
|
|||||||
@@ -5,8 +5,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<div id="codeMirrorContainer"></div>
|
<div id="codeMirrorContainer"></div>
|
||||||
|
|
||||||
<script src="../../y.js"></script>
|
<script src="../bower_components/yjs/y.js"></script>
|
||||||
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
|
|
||||||
<script src="../bower_components/codemirror/lib/codemirror.js"></script>
|
<script src="../bower_components/codemirror/lib/codemirror.js"></script>
|
||||||
<script src="../bower_components/codemirror/mode/javascript/javascript.js"></script>
|
<script src="../bower_components/codemirror/mode/javascript/javascript.js"></script>
|
||||||
<link rel="stylesheet" href="../bower_components/codemirror/lib/codemirror.css">
|
<link rel="stylesheet" href="../bower_components/codemirror/lib/codemirror.css">
|
||||||
|
|||||||
@@ -1,16 +1,24 @@
|
|||||||
/* global Y, CodeMirror */
|
/* global Y, CodeMirror */
|
||||||
|
|
||||||
let y = new Y('codemirror-example', {
|
// initialize a shared object. This function call returns a promise!
|
||||||
|
Y({
|
||||||
|
db: {
|
||||||
|
name: 'memory'
|
||||||
|
},
|
||||||
connector: {
|
connector: {
|
||||||
name: 'websockets-client',
|
name: 'websockets-client',
|
||||||
url: 'http://127.0.0.1:1234'
|
room: 'codemirror-example'
|
||||||
|
},
|
||||||
|
sourceDir: '/bower_components',
|
||||||
|
share: {
|
||||||
|
codemirror: 'Text' // y.share.codemirror is of type Y.Text
|
||||||
}
|
}
|
||||||
})
|
}).then(function (y) {
|
||||||
|
window.yCodeMirror = y
|
||||||
|
|
||||||
window.yCodeMirror = y
|
var editor = CodeMirror(document.querySelector('#codeMirrorContainer'), {
|
||||||
|
mode: 'javascript',
|
||||||
var editor = CodeMirror(document.querySelector('#codeMirrorContainer'), {
|
lineNumbers: true
|
||||||
mode: 'javascript',
|
})
|
||||||
lineNumbers: true
|
y.share.codemirror.bindCodeMirror(editor)
|
||||||
})
|
})
|
||||||
y.define('codemirror', Y.Text).bindCodeMirror(editor)
|
|
||||||
|
|||||||
@@ -13,8 +13,11 @@
|
|||||||
<button type="button" id="clearDrawingCanvas">Clear Drawing</button>
|
<button type="button" id="clearDrawingCanvas">Clear Drawing</button>
|
||||||
<svg id="drawingCanvas" viewbox="0 0 100 100" width="100%"></svg>
|
<svg id="drawingCanvas" viewbox="0 0 100 100" width="100%"></svg>
|
||||||
<script src="../../y.js"></script>
|
<script src="../../y.js"></script>
|
||||||
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
|
<script src="../../../y-array/y-array.js"></script>
|
||||||
<script src="../bower_components/d3/d3.min.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="./index.js"></script>
|
<script src="./index.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,74 +1,84 @@
|
|||||||
/* globals Y, d3 */
|
/* globals Y, d3 */
|
||||||
|
'strict mode'
|
||||||
|
|
||||||
let y = new Y('drawing-example', {
|
Y({
|
||||||
|
db: {
|
||||||
|
name: 'memory'
|
||||||
|
},
|
||||||
connector: {
|
connector: {
|
||||||
name: 'websockets-client',
|
name: 'websockets-client',
|
||||||
url: 'http://127.0.0.1:1234'
|
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')
|
||||||
|
|
||||||
window.yDrawing = y
|
var svg = d3.select('#drawingCanvas')
|
||||||
var drawing = y.define('drawing', Y.Array)
|
.call(d3.behavior.drag()
|
||||||
var renderPath = d3.svg.line()
|
.on('dragstart', dragstart)
|
||||||
.x(function (d) { return d[0] })
|
.on('drag', drag)
|
||||||
.y(function (d) { return d[1] })
|
.on('dragend', dragend))
|
||||||
.interpolate('basic')
|
|
||||||
|
|
||||||
var svg = d3.select('#drawingCanvas')
|
// create line from a shared array object and update the line when the array changes
|
||||||
.call(d3.behavior.drag()
|
function drawLine (yarray) {
|
||||||
.on('dragstart', dragstart)
|
var line = svg.append('path').datum(yarray.toArray())
|
||||||
.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)
|
line.attr('d', renderPath)
|
||||||
})
|
yarray.observe(function (event) {
|
||||||
}
|
// we only implement insert events that are appended to the end of the array
|
||||||
// call drawLine every time an array is appended
|
event.values.forEach(function (value) {
|
||||||
drawing.observe(function (event) {
|
line.datum().push(value)
|
||||||
event.removedElements.forEach(function () {
|
})
|
||||||
// if one is deleted, all will be deleted!!
|
line.attr('d', renderPath)
|
||||||
svg.selectAll('path').remove()
|
})
|
||||||
})
|
}
|
||||||
event.addedElements.forEach(function (path) {
|
// call drawLine every time an array is appended
|
||||||
drawLine(path)
|
y.share.drawing.observe(function (event) {
|
||||||
})
|
if (event.type === 'insert') {
|
||||||
})
|
event.values.forEach(drawLine)
|
||||||
// draw all existing content
|
} else {
|
||||||
for (var i = 0; i < drawing.length; i++) {
|
// just remove all elements (thats what we do anyway)
|
||||||
drawLine(drawing.get(i))
|
svg.selectAll('path').remove()
|
||||||
}
|
}
|
||||||
|
})
|
||||||
// clear canvas on request
|
// draw all existing content
|
||||||
document.querySelector('#clearDrawingCanvas').onclick = function () {
|
for (var i = 0; i < drawing.length; i++) {
|
||||||
drawing.delete(0, drawing.length)
|
drawLine(drawing.get(i))
|
||||||
}
|
|
||||||
|
|
||||||
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 () {
|
// clear canvas on request
|
||||||
sharedLine = null
|
document.querySelector('#clearDrawingCanvas').onclick = function () {
|
||||||
window.clearTimeout(ignoreDrag)
|
drawing.delete(0, drawing.length)
|
||||||
ignoreDrag = null
|
}
|
||||||
}
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
</head>
|
|
||||||
<script src="../../y.js"></script>
|
|
||||||
<script src='../../../y-websockets-client/y-websockets-client.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>
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
/* global Y, d3 */
|
|
||||||
|
|
||||||
const hooks = {
|
|
||||||
'magic-drawing': {
|
|
||||||
fillType: function (dom, type) {
|
|
||||||
initDrawingBindings(type, dom)
|
|
||||||
},
|
|
||||||
createDom: function (type) {
|
|
||||||
const dom = document.createElement('magic-drawing')
|
|
||||||
initDrawingBindings(type, dom)
|
|
||||||
return dom
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.onload = function () {
|
|
||||||
window.domBinding = new Y.DomBinding(window.yXmlType, document.body, { hooks })
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addMagicDrawing = function addMagicDrawing () {
|
|
||||||
let mt = document.createElement('magic-drawing')
|
|
||||||
mt.setAttribute('data-yjs-hook', '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.setAttribute('data-yjs-hook', '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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let y = new Y('html-editor-drawing-hook-example', {
|
|
||||||
connector: {
|
|
||||||
name: 'websockets-client',
|
|
||||||
url: 'http://127.0.0.1:1234'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
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,11 +1,11 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
</head>
|
</head>
|
||||||
<script src="./index.js" type="module"></script>
|
<!-- 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="./index.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body contenteditable="true">
|
||||||
<label for="room">Room: </label>
|
|
||||||
<input type="text" id="room" name="room">
|
|
||||||
<div id="content" contenteditable style="position:absolute;top:35px;left:0;right:0;bottom:0;outline: 0px solid transparent;"></div>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,77 +1,35 @@
|
|||||||
|
/* global Y */
|
||||||
|
|
||||||
import YWebsocketsConnector from '../../src/Connectors/WebsocketsConnector/WebsocketsConnector.js'
|
// initialize a shared object. This function call returns a promise!
|
||||||
import Y from '../../src/Y.js'
|
let y = new Y({
|
||||||
import DomBinding from '../../src/Bindings/DomBinding/DomBinding.js'
|
connector: {
|
||||||
import UndoManager from '../../src/Util/UndoManager.js'
|
name: 'websockets-client',
|
||||||
import YXmlFragment from '../../src/Types/YXml/YXmlFragment.js'
|
url: 'http://127.0.0.1:1234',
|
||||||
import YXmlText from '../../src/Types/YXml/YXmlText.js'
|
room: 'html-editor-example6'
|
||||||
import YXmlElement from '../../src/Types/YXml/YXmlElement.js'
|
// maxBufferLength: 100
|
||||||
import YIndexdDBPersistence from '../../src/Persistences/IndexedDBPersistence.js'
|
}
|
||||||
|
})
|
||||||
|
window.yXml = y
|
||||||
|
window.yXmlType = y.define('xml', Y.XmlFragment)
|
||||||
|
window.onload = function () {
|
||||||
|
console.log('start!')
|
||||||
|
// Bind children of XmlFragment to the document.body
|
||||||
|
window.yXmlType.bindToDom(document.body)
|
||||||
|
}
|
||||||
|
window.undoManager = new Y.utils.UndoManager(window.yXmlType, {
|
||||||
|
captureTimeout: 0
|
||||||
|
})
|
||||||
|
|
||||||
const connector = new YWebsocketsConnector()
|
document.onkeydown = function interceptUndoRedo (e) {
|
||||||
const persistence = new YIndexdDBPersistence()
|
if (e.keyCode === 90 && e.metaKey) {
|
||||||
|
console.log('uidtaren')
|
||||||
const roomInput = document.querySelector('#room')
|
if (!e.shiftKey) {
|
||||||
|
console.info('Undo!')
|
||||||
let currentRoomName = null
|
window.undoManager.undo()
|
||||||
let y = null
|
|
||||||
let domBinding = null
|
|
||||||
|
|
||||||
function setRoomName (roomName) {
|
|
||||||
if (currentRoomName !== roomName) {
|
|
||||||
console.log(`change room: "${roomName}"`)
|
|
||||||
roomInput.value = roomName
|
|
||||||
currentRoomName = roomName
|
|
||||||
location.hash = '#' + roomName
|
|
||||||
if (y !== null) {
|
|
||||||
domBinding.destroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
const room = connector._rooms.get(roomName)
|
|
||||||
if (room !== undefined) {
|
|
||||||
y = room.y
|
|
||||||
} else {
|
} else {
|
||||||
y = new Y(roomName, null, null, { gc: true })
|
console.info('Redo!')
|
||||||
persistence.connectY(roomName, y).then(() => {
|
window.undoManager.redo()
|
||||||
// connect after persisted content was applied to y
|
|
||||||
// If we don't wait for persistence, the other peer will send all data, waisting
|
|
||||||
// network bandwidth..
|
|
||||||
connector.connectY(roomName, y)
|
|
||||||
})
|
|
||||||
window.y = y
|
|
||||||
}
|
}
|
||||||
|
e.preventDefault()
|
||||||
window.y = y
|
|
||||||
window.yXmlType = y.define('xml', YXmlFragment)
|
|
||||||
|
|
||||||
domBinding = new DomBinding(window.yXmlType, document.querySelector('#content'), { scrollingElement: document.scrollingElement })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
window.setRoomName = setRoomName
|
|
||||||
|
|
||||||
window.createRooms = function (i = 0) {
|
|
||||||
setInterval(function () {
|
|
||||||
setRoomName(i + '')
|
|
||||||
i++
|
|
||||||
const nodes = []
|
|
||||||
for (let j = 0; j < 100; j++) {
|
|
||||||
const node = new YXmlElement('p')
|
|
||||||
node.insert(0, [new YXmlText(`This is the ${i}th paragraph of room ${i}`)])
|
|
||||||
nodes.push(node)
|
|
||||||
}
|
|
||||||
y.share.xml.insert(0, nodes)
|
|
||||||
}, 100)
|
|
||||||
}
|
|
||||||
|
|
||||||
connector.syncPersistence(persistence)
|
|
||||||
|
|
||||||
window.connector = connector
|
|
||||||
window.persistence = persistence
|
|
||||||
|
|
||||||
window.onload = function () {
|
|
||||||
setRoomName((location.hash || '#default').slice(1))
|
|
||||||
roomInput.addEventListener('input', e => {
|
|
||||||
const roomName = e.target.value
|
|
||||||
setRoomName(roomName)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -16,9 +16,6 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script src="../../y.js"></script>
|
<script type="module" src="./index.js"></script>
|
||||||
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
|
|
||||||
<script src='../../../y-indexeddb/y-indexeddb.js'></script>
|
|
||||||
<script src="./index.js"></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,19 +1,24 @@
|
|||||||
/* global Y, CodeMirror */
|
/* global Y, CodeMirror */
|
||||||
|
|
||||||
const persistence = new Y.IndexedDB()
|
// initialize a shared object. This function call returns a promise!
|
||||||
const connector = {
|
Y({
|
||||||
|
db: {
|
||||||
|
name: 'memory'
|
||||||
|
},
|
||||||
connector: {
|
connector: {
|
||||||
name: 'websockets-client',
|
name: 'websockets-client',
|
||||||
room: 'codemirror-example'
|
room: 'codemirror-example'
|
||||||
|
},
|
||||||
|
sourceDir: '/bower_components',
|
||||||
|
share: {
|
||||||
|
codemirror: 'Text' // y.share.codemirror is of type Y.Text
|
||||||
}
|
}
|
||||||
}
|
}).then(function (y) {
|
||||||
|
window.yCodeMirror = y
|
||||||
|
|
||||||
const y = new Y('codemirror-example', connector, persistence)
|
var editor = CodeMirror(document.querySelector('#codeMirrorContainer'), {
|
||||||
window.yCodeMirror = y
|
mode: 'javascript',
|
||||||
|
lineNumbers: true
|
||||||
var editor = CodeMirror(document.querySelector('#codeMirrorContainer'), {
|
})
|
||||||
mode: 'javascript',
|
y.share.codemirror.bindCodeMirror(editor)
|
||||||
lineNumbers: true
|
|
||||||
})
|
})
|
||||||
|
|
||||||
y.define('codemirror', Y.Text).bindCodeMirror(editor)
|
|
||||||
|
|||||||
@@ -49,7 +49,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="../../y.js"></script>
|
<script src="../../y.js"></script>
|
||||||
<script src='../../../y-websockets-client/y-websockets-client.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-websockets-client/y-websockets-client.js"></script>
|
||||||
<script src="./index.js"></script>
|
<script src="./index.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,38 +1,64 @@
|
|||||||
/* global Y */
|
/* global Y */
|
||||||
|
|
||||||
function bindYjsInstance (y, suffix) {
|
Y({
|
||||||
y.define('textarea', Y.Text).bind(document.getElementById('textarea' + suffix))
|
db: {
|
||||||
|
name: 'memory'
|
||||||
|
},
|
||||||
|
connector: {
|
||||||
|
name: 'websockets-client',
|
||||||
|
room: 'Textarea-example',
|
||||||
|
url: 'https://yjs-v13.herokuapp.com/'
|
||||||
|
},
|
||||||
|
share: {
|
||||||
|
textarea: 'Text'
|
||||||
|
}
|
||||||
|
}).then(function (y) {
|
||||||
|
window.y1 = y
|
||||||
|
y.share.textarea.bind(document.getElementById('textarea1'))
|
||||||
|
})
|
||||||
|
|
||||||
|
Y({
|
||||||
|
db: {
|
||||||
|
name: 'memory'
|
||||||
|
},
|
||||||
|
connector: {
|
||||||
|
name: 'websockets-client',
|
||||||
|
room: 'Textarea-example',
|
||||||
|
url: 'https://yjs-v13-second.herokuapp.com/'
|
||||||
|
},
|
||||||
|
share: {
|
||||||
|
textarea: 'Text'
|
||||||
|
}
|
||||||
|
}).then(function (y) {
|
||||||
|
window.y2 = y
|
||||||
|
y.share.textarea.bind(document.getElementById('textarea2'))
|
||||||
y.connector.socket.on('connection', function () {
|
y.connector.socket.on('connection', function () {
|
||||||
document.getElementById('container' + suffix).removeAttribute('disconnected')
|
document.getElementById('container2').removeAttribute('disconnected')
|
||||||
})
|
})
|
||||||
y.connector.socket.on('disconnect', function () {
|
y.connector.socket.on('disconnect', function () {
|
||||||
document.getElementById('container' + suffix).setAttribute('disconnected', true)
|
document.getElementById('container2').setAttribute('disconnected', true)
|
||||||
})
|
})
|
||||||
}
|
})
|
||||||
|
|
||||||
let y1 = new Y('infinite-example', {
|
Y({
|
||||||
|
db: {
|
||||||
|
name: 'memory'
|
||||||
|
},
|
||||||
connector: {
|
connector: {
|
||||||
name: 'websockets-client',
|
name: 'websockets-client',
|
||||||
url: 'http://127.0.0.1:1234'
|
room: 'Textarea-example',
|
||||||
|
url: 'https://yjs-v13-third.herokuapp.com/'
|
||||||
|
},
|
||||||
|
share: {
|
||||||
|
textarea: 'Text'
|
||||||
}
|
}
|
||||||
|
}).then(function (y) {
|
||||||
|
window.y3 = y
|
||||||
|
y.share.textarea.bind(document.getElementById('textarea3'))
|
||||||
|
y.connector.socket.on('connection', function () {
|
||||||
|
document.getElementById('container3').removeAttribute('disconnected')
|
||||||
|
})
|
||||||
|
y.connector.socket.on('disconnect', function () {
|
||||||
|
document.getElementById('container3').setAttribute('disconnected', true)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
window.y1 = y1
|
|
||||||
bindYjsInstance(y1, '1')
|
|
||||||
|
|
||||||
let y2 = new Y('infinite-example', {
|
|
||||||
connector: {
|
|
||||||
name: 'websockets-client',
|
|
||||||
url: 'http://127.0.0.1:1234'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
window.y2 = y2
|
|
||||||
bindYjsInstance(y2, '2')
|
|
||||||
|
|
||||||
let y3 = new Y('infinite-example', {
|
|
||||||
connector: {
|
|
||||||
name: 'websockets-client',
|
|
||||||
url: 'http://127.0.0.1:1234'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
window.y3 = y3
|
|
||||||
bindYjsInstance(y1, '3')
|
|
||||||
|
|||||||
@@ -17,7 +17,9 @@
|
|||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
<script src="../../y.js"></script>
|
<script src="../../y.js"></script>
|
||||||
<script src='../../../y-websockets-client/y-websockets-client.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="../bower_components/d3/d3.js"></script>
|
||||||
<script src="./index.js"></script>
|
<script src="./index.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -1,67 +1,74 @@
|
|||||||
|
/* @flow */
|
||||||
/* global Y, d3 */
|
/* global Y, d3 */
|
||||||
|
|
||||||
let y = new Y('jigsaw-example', {
|
// initialize a shared object. This function call returns a promise!
|
||||||
|
Y({
|
||||||
|
db: {
|
||||||
|
name: 'memory'
|
||||||
|
},
|
||||||
connector: {
|
connector: {
|
||||||
name: 'websockets-client',
|
name: 'websockets-client',
|
||||||
url: 'http://127.0.0.1:1234'
|
room: 'Puzzle-example',
|
||||||
|
url: 'http://localhost:1234'
|
||||||
|
},
|
||||||
|
share: {
|
||||||
|
piece1: 'Map',
|
||||||
|
piece2: 'Map',
|
||||||
|
piece3: 'Map',
|
||||||
|
piece4: 'Map'
|
||||||
}
|
}
|
||||||
})
|
}).then(function (y) {
|
||||||
|
window.yJigsaw = y
|
||||||
|
var origin // mouse start position - translation of piece
|
||||||
|
var drag = d3.behavior.drag()
|
||||||
|
.on('dragstart', function (params) {
|
||||||
|
// get the translation of the element
|
||||||
|
var translation = d3
|
||||||
|
.select(this)
|
||||||
|
.attr('transform')
|
||||||
|
.slice(10, -1)
|
||||||
|
.split(',')
|
||||||
|
.map(Number)
|
||||||
|
// mouse coordinates
|
||||||
|
var mouse = d3.mouse(this.parentNode)
|
||||||
|
origin = {
|
||||||
|
x: mouse[0] - translation[0],
|
||||||
|
y: mouse[1] - translation[1]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on('drag', function () {
|
||||||
|
var mouse = d3.mouse(this.parentNode)
|
||||||
|
var x = mouse[0] - origin.x // =^= mouse - mouse at dragstart + translation at dragstart
|
||||||
|
var y = mouse[1] - origin.y
|
||||||
|
d3.select(this).attr('transform', 'translate(' + x + ',' + y + ')')
|
||||||
|
})
|
||||||
|
.on('dragend', function (piece, i) {
|
||||||
|
// save the current translation of the puzzle piece
|
||||||
|
var mouse = d3.mouse(this.parentNode)
|
||||||
|
var x = mouse[0] - origin.x
|
||||||
|
var y = mouse[1] - origin.y
|
||||||
|
piece.set('translation', {x: x, y: y})
|
||||||
|
})
|
||||||
|
|
||||||
let jigsaw = y.define('jigsaw', Y.Map)
|
var data = [y.share.piece1, y.share.piece2, y.share.piece3, y.share.piece4]
|
||||||
window.yJigsaw = y
|
var pieces = d3.select(document.querySelector('#puzzle-example')).selectAll('path').data(data)
|
||||||
|
|
||||||
var origin // mouse start position - translation of piece
|
pieces
|
||||||
var drag = d3.behavior.drag()
|
.classed('draggable', true)
|
||||||
.on('dragstart', function (params) {
|
.attr('transform', function (piece) {
|
||||||
// get the translation of the element
|
var translation = piece.get('translation') || {x: 0, y: 0}
|
||||||
var translation = d3
|
return 'translate(' + translation.x + ',' + translation.y + ')'
|
||||||
.select(this)
|
}).call(drag)
|
||||||
.attr('transform')
|
|
||||||
.slice(10, -1)
|
|
||||||
.split(',')
|
|
||||||
.map(Number)
|
|
||||||
// mouse coordinates
|
|
||||||
var mouse = d3.mouse(this.parentNode)
|
|
||||||
origin = {
|
|
||||||
x: mouse[0] - translation[0],
|
|
||||||
y: mouse[1] - translation[1]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.on('drag', function () {
|
|
||||||
var mouse = d3.mouse(this.parentNode)
|
|
||||||
var x = mouse[0] - origin.x // =^= mouse - mouse at dragstart + translation at dragstart
|
|
||||||
var y = mouse[1] - origin.y
|
|
||||||
d3.select(this).attr('transform', 'translate(' + x + ',' + y + ')')
|
|
||||||
})
|
|
||||||
.on('dragend', function (piece, i) {
|
|
||||||
// save the current translation of the puzzle piece
|
|
||||||
var mouse = d3.mouse(this.parentNode)
|
|
||||||
var x = mouse[0] - origin.x
|
|
||||||
var y = mouse[1] - origin.y
|
|
||||||
jigsaw.set(piece, {x: x, y: y})
|
|
||||||
})
|
|
||||||
|
|
||||||
var data = ['piece1', 'piece2', 'piece3', 'piece4']
|
data.forEach(function (piece) {
|
||||||
var pieces = d3.select(document.querySelector('#puzzle-example')).selectAll('path').data(data)
|
piece.observe(function () {
|
||||||
|
// whenever a property of a piece changes, update the translation of the pieces
|
||||||
pieces
|
pieces
|
||||||
.classed('draggable', true)
|
.transition()
|
||||||
.attr('transform', function (piece) {
|
.attr('transform', function (piece) {
|
||||||
var translation = piece.get('translation') || {x: 0, y: 0}
|
var translation = piece.get('translation') || {x: 0, y: 0}
|
||||||
return 'translate(' + translation.x + ',' + translation.y + ')'
|
return 'translate(' + translation.x + ',' + translation.y + ')'
|
||||||
}).call(drag)
|
})
|
||||||
|
})
|
||||||
data.forEach(function (piece) {
|
|
||||||
jigsaw.observe(function () {
|
|
||||||
// whenever a property of a piece changes, update the translation of the pieces
|
|
||||||
pieces
|
|
||||||
.transition()
|
|
||||||
.attr('transform', function (piece) {
|
|
||||||
var translation = piece.get(piece)
|
|
||||||
if (translation == null || typeof translation.x !== 'number' || typeof translation.y !== 'number') {
|
|
||||||
translation = { x: 0, y: 0 }
|
|
||||||
}
|
|
||||||
return 'translate(' + translation.x + ',' + translation.y + ')'
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -13,8 +13,11 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script src="../../y.js"></script>
|
<script src="../bower_components/yjs/y.js"></script>
|
||||||
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
|
<script src="../bower_components/y-array/y-array.js"></script>
|
||||||
|
<script src="../bower_components/y-text/y-text.js"></script>
|
||||||
|
<script src="../bower_components/y-websockets-client/y-websockets-client.js"></script>
|
||||||
|
<script src="../bower_components/y-memory/y-memory.js"></script>
|
||||||
<script src="../node_modules/monaco-editor/min/vs/loader.js"></script>
|
<script src="../node_modules/monaco-editor/min/vs/loader.js"></script>
|
||||||
<script src="./index.js"></script>
|
<script src="./index.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -2,21 +2,29 @@
|
|||||||
|
|
||||||
require.config({ paths: { 'vs': '../node_modules/monaco-editor/min/vs' } })
|
require.config({ paths: { 'vs': '../node_modules/monaco-editor/min/vs' } })
|
||||||
|
|
||||||
let y = new Y('monaco-example', {
|
|
||||||
connector: {
|
|
||||||
name: 'websockets-client',
|
|
||||||
url: 'http://127.0.0.1:1234'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
require(['vs/editor/editor.main'], function () {
|
require(['vs/editor/editor.main'], function () {
|
||||||
window.yMonaco = y
|
// Initialize a shared object. This function call returns a promise!
|
||||||
|
Y({
|
||||||
|
db: {
|
||||||
|
name: 'memory'
|
||||||
|
},
|
||||||
|
connector: {
|
||||||
|
name: 'websockets-client',
|
||||||
|
room: 'monaco-example'
|
||||||
|
},
|
||||||
|
sourceDir: '/bower_components',
|
||||||
|
share: {
|
||||||
|
monaco: 'Text' // y.share.monaco is of type Y.Text
|
||||||
|
}
|
||||||
|
}).then(function (y) {
|
||||||
|
window.yMonaco = y
|
||||||
|
|
||||||
// Create Monaco editor
|
// Create Monaco editor
|
||||||
var editor = monaco.editor.create(document.getElementById('monacoContainer'), {
|
var editor = monaco.editor.create(document.getElementById('monacoContainer'), {
|
||||||
language: 'javascript'
|
language: 'javascript'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Bind to y.share.monaco
|
||||||
|
y.share.monaco.bindMonaco(editor)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Bind to y.share.monaco
|
|
||||||
y.define('monaco', Y.Text).bindMonaco(editor)
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
</head>
|
|
||||||
<script src="./index.js" type="module"></script>
|
|
||||||
<link rel="stylesheet" type="text/css" href="style.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="sidebar">
|
|
||||||
<h3 id="createNoteButton">+ Create Note</h3>
|
|
||||||
<div class="notelist"></div>
|
|
||||||
</div>
|
|
||||||
<div class="main">
|
|
||||||
<h1 id="headline"></h1>
|
|
||||||
<div id="editor" contenteditable="true"></div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
/* eslint-env browser */
|
|
||||||
|
|
||||||
import { createYdbClient } from '../../YdbClient/index.js'
|
|
||||||
import Y from '../../src/Y.dist.js'
|
|
||||||
import * as ydb from '../../YdbClient/YdbClient.js'
|
|
||||||
import DomBinding from '../../src/Bindings/DomBinding/DomBinding.js'
|
|
||||||
|
|
||||||
const uuidv4 = () => 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
|
|
||||||
const r = Math.random() * 16 | 0
|
|
||||||
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16)
|
|
||||||
})
|
|
||||||
|
|
||||||
createYdbClient('ws://localhost:8899/ws').then(ydbclient => {
|
|
||||||
const y = ydbclient.getY('notelist')
|
|
||||||
let ynotelist = y.define('notelist', Y.Array)
|
|
||||||
window.ynotelist = ynotelist
|
|
||||||
const domNoteList = document.querySelector('.notelist')
|
|
||||||
|
|
||||||
// utils
|
|
||||||
const addEventListener = (element, eventname, f) => element.addEventListener(eventname, f)
|
|
||||||
|
|
||||||
// create note button
|
|
||||||
const createNoteButton = event => {
|
|
||||||
ynotelist.insert(0, [{
|
|
||||||
guid: uuidv4(),
|
|
||||||
title: 'Note #' + ynotelist.length
|
|
||||||
}])
|
|
||||||
}
|
|
||||||
addEventListener(document.querySelector('#createNoteButton'), 'click', createNoteButton)
|
|
||||||
window.createNote = createNoteButton
|
|
||||||
window.createNotes = n => {
|
|
||||||
y.transact(() => {
|
|
||||||
for (let i = 0; i < n; i++) {
|
|
||||||
createNoteButton()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// clear note list function
|
|
||||||
window.clearNotes = () => ynotelist.delete(0, ynotelist.length)
|
|
||||||
|
|
||||||
// update editor and editor title
|
|
||||||
let domBinding = null
|
|
||||||
const updateEditor = () => {
|
|
||||||
domNoteList.querySelectorAll('a').forEach(a => a.classList.remove('selected'))
|
|
||||||
const domNote = document.querySelector('.notelist').querySelector(`[href="${location.hash}"]`)
|
|
||||||
if (domNote !== null) {
|
|
||||||
domNote.classList.add('selected')
|
|
||||||
const note = ynotelist.toArray().find(note => note.guid === location.hash.slice(1))
|
|
||||||
if (note !== undefined) {
|
|
||||||
const ydoc = ydbclient.getY(note.guid)
|
|
||||||
const ycontent = ydoc.define('content', Y.XmlFragment)
|
|
||||||
if (domBinding !== null) {
|
|
||||||
domBinding.destroy()
|
|
||||||
}
|
|
||||||
domBinding = new DomBinding(ycontent, document.querySelector('#editor'))
|
|
||||||
document.querySelector('#headline').innerText = note.title
|
|
||||||
document.querySelector('#editor').focus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// listen to url-hash changes
|
|
||||||
addEventListener(window, 'hashchange', updateEditor)
|
|
||||||
updateEditor()
|
|
||||||
|
|
||||||
const styleSyncedState = (div, noteSyncedState) => {
|
|
||||||
let classes = []
|
|
||||||
if (noteSyncedState.persisted) {
|
|
||||||
classes.push('persisted')
|
|
||||||
} else {
|
|
||||||
if (noteSyncedState.upsynced) {
|
|
||||||
classes.push('upsynced')
|
|
||||||
} else {
|
|
||||||
classes.push('noupsynced')
|
|
||||||
}
|
|
||||||
if (noteSyncedState.downsynced) {
|
|
||||||
classes.push('downsynced')
|
|
||||||
} else {
|
|
||||||
classes.push('nodownsynced')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
div.setAttribute('class', classes.join(' '))
|
|
||||||
}
|
|
||||||
|
|
||||||
ydbclient.on('syncstate', event => event.updated.forEach((state, room) => {
|
|
||||||
const a = document.querySelector(`[href="#${room}"]`)
|
|
||||||
if (a !== null) {
|
|
||||||
styleSyncedState(a.firstChild, state)
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
// render note list
|
|
||||||
const renderNoteList = (elementList, insertRef = domNoteList.firstChild) => {
|
|
||||||
const fragment = document.createDocumentFragment()
|
|
||||||
const addNow = elementList.splice(0, 100)
|
|
||||||
addNow.forEach(note => {
|
|
||||||
const a = document.createElement('a')
|
|
||||||
const div = document.createElement('div')
|
|
||||||
a.insertBefore(div, null)
|
|
||||||
a.setAttribute('href', '#' + note.guid)
|
|
||||||
div.innerText = note.title
|
|
||||||
styleSyncedState(div, ydbclient.getRoomState(note.guid))
|
|
||||||
fragment.insertBefore(a, null)
|
|
||||||
})
|
|
||||||
if (domBinding == null) {
|
|
||||||
updateEditor()
|
|
||||||
}
|
|
||||||
domNoteList.insertBefore(fragment, insertRef)
|
|
||||||
if (elementList.length > 0) {
|
|
||||||
setTimeout(() => renderNoteList(elementList, insertRef), 100)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
{
|
|
||||||
const notelist = ynotelist.toArray()
|
|
||||||
if (notelist.length > 0) {
|
|
||||||
renderNoteList(notelist)
|
|
||||||
ydb.subscribeRooms(ydbclient, notelist.map(note => note.guid))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ynotelist.observe(event => {
|
|
||||||
const addedNotes = []
|
|
||||||
event.addedElements.forEach(itemJson => itemJson._content.forEach(json => addedNotes.push(json)))
|
|
||||||
renderNoteList(addedNotes.slice().reverse()) // renderNoteList modifies addedNotes, so first make a copy of it
|
|
||||||
setTimeout(() => {
|
|
||||||
ydb.subscribeRooms(ydbclient, addedNotes.map(note => note.guid))
|
|
||||||
}, 200)
|
|
||||||
if (domBinding === null) {
|
|
||||||
updateEditor()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
.sidebar {
|
|
||||||
height: 100%; /* Full-height: remove this if you want "auto" height */
|
|
||||||
width: 180px; /* Set the width of the sidebar */
|
|
||||||
position: fixed; /* Fixed Sidebar (stay in place on scroll) */
|
|
||||||
z-index: 1; /* Stay on top */
|
|
||||||
top: 0; /* Stay at the top */
|
|
||||||
left: 0;
|
|
||||||
background-color: #111; /* Black */
|
|
||||||
overflow-x: hidden; /* Disable horizontal scroll */
|
|
||||||
padding-top: 20px;
|
|
||||||
color: #50abff;
|
|
||||||
}
|
|
||||||
|
|
||||||
#createNoteButton {
|
|
||||||
padding-left: .5em;
|
|
||||||
padding-top: .5em;
|
|
||||||
padding-bottom: .7em;
|
|
||||||
margin: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notelist > a {
|
|
||||||
padding: 6px 8px 6px 16px;
|
|
||||||
text-decoration: none;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #818181;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notelist > a.selected {
|
|
||||||
border-style: outset;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notelist > a > div {
|
|
||||||
position: relative;
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* When you mouse over the navigation links, change their color */
|
|
||||||
.sidebar a:hover {
|
|
||||||
color: #f1f1f1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Style page content */
|
|
||||||
.main {
|
|
||||||
margin-left: 180px; /* Same as the width of the sidebar */
|
|
||||||
padding: 0px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* On smaller screens, where height is less than 450px, change the style of the sidebar (less padding and a smaller font size) */
|
|
||||||
@media screen and (max-height: 450px) {
|
|
||||||
.sidebar {padding-top: 15px;}
|
|
||||||
.sidebar a {font-size: 18px;}
|
|
||||||
}
|
|
||||||
|
|
||||||
#editor {
|
|
||||||
min-height: 400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[contenteditable]:focus {
|
|
||||||
outline: 0px solid transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.persisted::before {
|
|
||||||
content: "✔";
|
|
||||||
color: green;
|
|
||||||
position: absolute;
|
|
||||||
right: -14px;
|
|
||||||
top: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upsynced::before {
|
|
||||||
content: "↑";
|
|
||||||
color: green;
|
|
||||||
position: absolute;
|
|
||||||
right: -14px;
|
|
||||||
top: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.noupsynced::before {
|
|
||||||
content: "↑";
|
|
||||||
color: red;
|
|
||||||
position: absolute;
|
|
||||||
right: -14px;
|
|
||||||
top: 0px;
|
|
||||||
}
|
|
||||||
.downsynced::after {
|
|
||||||
content: "↓";
|
|
||||||
color: green;
|
|
||||||
position: absolute;
|
|
||||||
right: -22px;
|
|
||||||
top: 0px;
|
|
||||||
}
|
|
||||||
.nodownsynced::after {
|
|
||||||
content: "↓";
|
|
||||||
color: red;
|
|
||||||
position: absolute;
|
|
||||||
right: -22px;
|
|
||||||
top: 0px;
|
|
||||||
}
|
|
||||||
1173
examples/package-lock.json
generated
Normal file
1173
examples/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -9,15 +9,12 @@
|
|||||||
"author": "Kevin Jahns",
|
"author": "Kevin Jahns",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"monaco-editor": "^0.8.3",
|
"monaco-editor": "^0.8.3"
|
||||||
"rollup": "^0.52.3"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"standard": "^10.0.2"
|
"standard": "^10.0.2"
|
||||||
},
|
},
|
||||||
"standard": {
|
"standard": {
|
||||||
"ignore": [
|
"ignore": ["bower_components"]
|
||||||
"bower_components"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<!-- Main quill library -->
|
|
||||||
<script src="../../node_modules/quill/dist/quill.min.js"></script>
|
|
||||||
<link href="../../node_modules/quill/dist/quill.snow.css" rel="stylesheet">
|
|
||||||
<!-- Quill cursors module -->
|
|
||||||
<script src="../../node_modules/quill-cursors/dist/quill-cursors.min.js"></script>
|
|
||||||
<link href="../../node_modules/quill-cursors/dist/quill-cursors.css" rel="stylesheet">
|
|
||||||
<!-- Yjs Library and connector -->
|
|
||||||
<script src="../../y.js"></script>
|
|
||||||
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="quill-container">
|
|
||||||
<div id="quill">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script src="./index.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
/* global Y, Quill, QuillCursors */
|
|
||||||
|
|
||||||
Quill.register('modules/cursors', QuillCursors)
|
|
||||||
|
|
||||||
let y = new Y('quill-0', {
|
|
||||||
connector: {
|
|
||||||
name: 'websockets-client',
|
|
||||||
url: 'http://127.0.0.1:1234'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
let users = y.define('users', Y.Array)
|
|
||||||
let myUserInfo = new Y.Map()
|
|
||||||
myUserInfo.set('name', 'dada')
|
|
||||||
myUserInfo.set('color', 'red')
|
|
||||||
users.push([myUserInfo])
|
|
||||||
|
|
||||||
let quill = new Quill('#quill-container', {
|
|
||||||
modules: {
|
|
||||||
toolbar: [
|
|
||||||
[{ header: [1, 2, false] }],
|
|
||||||
['bold', 'italic', 'underline'],
|
|
||||||
['image', 'code-block'],
|
|
||||||
[{ color: [] }, { background: [] }], // Snow theme fills in values
|
|
||||||
[{ script: 'sub' }, { script: 'super' }],
|
|
||||||
['link', 'image'],
|
|
||||||
['link', 'code-block'],
|
|
||||||
[{ list: 'ordered' }, { list: 'bullet' }]
|
|
||||||
],
|
|
||||||
cursors: {
|
|
||||||
hideDelay: 500
|
|
||||||
}
|
|
||||||
},
|
|
||||||
placeholder: 'Compose an epic...',
|
|
||||||
theme: 'snow' // or 'bubble'
|
|
||||||
})
|
|
||||||
|
|
||||||
let cursors = quill.getModule('cursors')
|
|
||||||
|
|
||||||
function drawCursors () {
|
|
||||||
cursors.clearCursors()
|
|
||||||
users.map((user, userId) => {
|
|
||||||
if (user !== myUserInfo) {
|
|
||||||
let relativeRange = user.get('range')
|
|
||||||
let lastUpdated = new Date(user.get('last updated'))
|
|
||||||
if (lastUpdated != null && new Date() - lastUpdated < 20000 && relativeRange != null) {
|
|
||||||
let start = Y.utils.fromRelativePosition(y, relativeRange.start).offset
|
|
||||||
let end = Y.utils.fromRelativePosition(y, relativeRange.end).offset
|
|
||||||
let range = { index: start, length: end - start }
|
|
||||||
cursors.setCursor(userId + '', range, user.get('name'), user.get('color'))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
users.observeDeep(drawCursors)
|
|
||||||
drawCursors()
|
|
||||||
|
|
||||||
quill.on('selection-change', function (range) {
|
|
||||||
if (range != null) {
|
|
||||||
myUserInfo.set('range', {
|
|
||||||
start: Y.utils.getRelativePosition(yText, range.index),
|
|
||||||
end: Y.utils.getRelativePosition(yText, range.index + range.length)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
myUserInfo.delete('range')
|
|
||||||
}
|
|
||||||
myUserInfo.set('last updated', new Date().toString())
|
|
||||||
})
|
|
||||||
|
|
||||||
let yText = y.define('quill', Y.Text)
|
|
||||||
let quillBinding = new Y.QuillBinding(yText, quill)
|
|
||||||
|
|
||||||
window.quillBinding = quillBinding
|
|
||||||
window.yText = yText
|
|
||||||
window.y = y
|
|
||||||
window.quill = quill
|
|
||||||
window.users = users
|
|
||||||
window.cursors = cursors
|
|
||||||
@@ -1,18 +1,35 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<!-- Main Quill library -->
|
<!-- quill does not include dist files! We are using the hosted version instead -->
|
||||||
<script src="../../node_modules/quill/dist/quill.min.js"></script>
|
<!--link rel="stylesheet" href="../bower_components/quill/dist/quill.snow.css" /-->
|
||||||
<link href="../../node_modules/quill/dist/quill.snow.css" rel="stylesheet">
|
<link href="https://cdn.quilljs.com/1.0.4/quill.snow.css" rel="stylesheet">
|
||||||
<!-- Yjs Library and connector -->
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.5.1/katex.min.css" rel="stylesheet">
|
||||||
<script src="../../y.js"></script>
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.2.0/styles/monokai-sublime.min.css" rel="stylesheet">
|
||||||
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
|
<style>
|
||||||
|
#quill-container {
|
||||||
|
border: 1px solid gray;
|
||||||
|
box-shadow: 0px 0px 10px gray;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="quill-container">
|
<div id="quill-container">
|
||||||
<div id="quill">
|
<div id="quill">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.5.1/katex.min.js" type="text/javascript"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.2.0/highlight.min.js" type="text/javascript"></script>
|
||||||
|
<script src="https://cdn.quilljs.com/1.0.4/quill.js"></script>
|
||||||
|
<!-- quill does not include dist files! We are using the hosted version instead (see above)
|
||||||
|
<script src="../bower_components/quill/dist/quill.js"></script>
|
||||||
|
-->
|
||||||
|
<script src="../../y.js"></script>
|
||||||
|
<script src="../../../y-array/y-array.js"></script>
|
||||||
|
<script src="../../../y-richtext/dist/y-richtext.js"></script>
|
||||||
|
<script src="../../../y-memory/y-memory.js"></script>
|
||||||
|
<script src="../../../y-websockets-client/y-websockets-client.js"></script>
|
||||||
<script src="./index.js"></script>
|
<script src="./index.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,33 +1,40 @@
|
|||||||
/* global Y, Quill */
|
/* global Y, Quill */
|
||||||
|
|
||||||
let y = new Y('quill-cursors-0', {
|
// initialize a shared object. This function call returns a promise!
|
||||||
|
|
||||||
|
Y({
|
||||||
|
db: {
|
||||||
|
name: 'memory'
|
||||||
|
},
|
||||||
connector: {
|
connector: {
|
||||||
name: 'websockets-client',
|
name: 'websockets-client',
|
||||||
url: 'http://127.0.0.1:1234'
|
room: 'richtext-example-quill-1.0-test',
|
||||||
}
|
url: 'http://localhost:1234'
|
||||||
})
|
|
||||||
|
|
||||||
let quill = new Quill('#quill-container', {
|
|
||||||
modules: {
|
|
||||||
toolbar: [
|
|
||||||
[{ header: [1, 2, false] }],
|
|
||||||
['bold', 'italic', 'underline'],
|
|
||||||
['image', 'code-block'],
|
|
||||||
[{ color: [] }, { background: [] }], // Snow theme fills in values
|
|
||||||
[{ script: 'sub' }, { script: 'super' }],
|
|
||||||
['link', 'image'],
|
|
||||||
['link', 'code-block'],
|
|
||||||
[{ list: 'ordered' }, { list: 'bullet' }]
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
placeholder: 'Compose an epic...',
|
sourceDir: '/bower_components',
|
||||||
theme: 'snow' // or 'bubble'
|
share: {
|
||||||
|
richtext: 'Richtext' // y.share.richtext is of type Y.Richtext
|
||||||
|
}
|
||||||
|
}).then(function (y) {
|
||||||
|
window.yQuill = y
|
||||||
|
|
||||||
|
// create quill element
|
||||||
|
window.quill = new Quill('#quill', {
|
||||||
|
modules: {
|
||||||
|
formula: true,
|
||||||
|
syntax: true,
|
||||||
|
toolbar: [
|
||||||
|
[{ size: ['small', false, 'large', 'huge'] }],
|
||||||
|
['bold', 'italic', 'underline'],
|
||||||
|
[{ color: [] }, { background: [] }], // Snow theme fills in values
|
||||||
|
[{ script: 'sub' }, { script: 'super' }],
|
||||||
|
['link', 'image'],
|
||||||
|
['link', 'code-block'],
|
||||||
|
[{ list: 'ordered' }]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
theme: 'snow'
|
||||||
|
})
|
||||||
|
// bind quill to richtext type
|
||||||
|
y.share.richtext.bind(window.quill)
|
||||||
})
|
})
|
||||||
|
|
||||||
let yText = y.define('quill', Y.Text)
|
|
||||||
|
|
||||||
let quillBinding = new Y.QuillBinding(yText, quill)
|
|
||||||
window.quillBinding = quillBinding
|
|
||||||
window.yText = yText
|
|
||||||
window.y = y
|
|
||||||
window.quill = quill
|
|
||||||
|
|||||||
@@ -4,12 +4,10 @@ import commonjs from 'rollup-plugin-commonjs'
|
|||||||
var pkg = require('./package.json')
|
var pkg = require('./package.json')
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
input: 'yjs-dist.js',
|
entry: 'yjs-dist.esm',
|
||||||
name: 'Y',
|
dest: 'yjs-dist.js',
|
||||||
output: {
|
moduleName: 'Y',
|
||||||
file: 'yjs-dist.js',
|
format: 'umd',
|
||||||
format: 'umd'
|
|
||||||
},
|
|
||||||
plugins: [
|
plugins: [
|
||||||
nodeResolve({
|
nodeResolve({
|
||||||
main: true,
|
main: true,
|
||||||
@@ -18,7 +16,7 @@ export default {
|
|||||||
}),
|
}),
|
||||||
commonjs()
|
commonjs()
|
||||||
],
|
],
|
||||||
sourcemap: true,
|
sourceMap: true,
|
||||||
banner: `
|
banner: `
|
||||||
/**
|
/**
|
||||||
* ${pkg.name} - ${pkg.description}
|
* ${pkg.name} - ${pkg.description}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ Y({
|
|||||||
toolbar: [
|
toolbar: [
|
||||||
[{ size: ['small', false, 'large', 'huge'] }],
|
[{ size: ['small', false, 'large', 'huge'] }],
|
||||||
['bold', 'italic', 'underline'],
|
['bold', 'italic', 'underline'],
|
||||||
[{ color: [] }, { background: [] }], // Snow theme fills in values
|
[{ color: [] }, { background: [] }], // Snow theme fills in values
|
||||||
[{ script: 'sub' }, { script: 'super' }],
|
[{ script: 'sub' }, { script: 'super' }],
|
||||||
['link', 'image'],
|
['link', 'image'],
|
||||||
['link', 'code-block'],
|
['link', 'code-block'],
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<body>
|
<body>
|
||||||
<textarea style="width:80%;" rows=40 autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
|
<textarea style="width:80%;" rows=40 id="textfield" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
|
||||||
<script type="module" src="./index.js"></script>
|
<script src="../../y.js"></script>
|
||||||
|
<script src="../../../y-array/y-array.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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,13 +1,23 @@
|
|||||||
/* eslint-env browser */
|
/* global Y */
|
||||||
import * as Y from '../../src/index.js'
|
|
||||||
import WebsocketProvider from '../../provider/websocket/WebSocketProvider.js'
|
|
||||||
|
|
||||||
const provider = new WebsocketProvider('ws://localhost:1234/')
|
// initialize a shared object. This function call returns a promise!
|
||||||
const ydocument = provider.get('textarea')
|
Y({
|
||||||
const type = ydocument.define('textarea', Y.Text)
|
db: {
|
||||||
const textarea = document.querySelector('textarea')
|
name: 'memory'
|
||||||
const binding = new Y.TextareaBinding(type, textarea)
|
},
|
||||||
|
connector: {
|
||||||
|
name: 'websockets-client',
|
||||||
|
room: 'Textarea-example2',
|
||||||
|
// url: '//localhost:1234',
|
||||||
|
url: 'https://yjs-v13.herokuapp.com/'
|
||||||
|
},
|
||||||
|
share: {
|
||||||
|
textarea: 'Text'
|
||||||
|
},
|
||||||
|
timeout: 5000 // reject if no connection was established within 5 seconds
|
||||||
|
}).then(function (y) {
|
||||||
|
window.yTextarea = y
|
||||||
|
|
||||||
window.textareaExample = {
|
// bind the textarea to a shared text element
|
||||||
provider, ydocument, type, textarea, binding
|
y.share.textarea.bind(document.getElementById('textfield'))
|
||||||
}
|
})
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
</head>
|
</head>
|
||||||
<!-- jquery is not required for YXml. It is just here for convenience, and to test batch operations. -->
|
<!-- 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="https://code.jquery.com/jquery-3.2.1.min.js"></script>
|
||||||
<script src="../../y.js"></script>
|
<script src="../yjs-dist.js"></script>
|
||||||
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
|
|
||||||
<script src="./index.js"></script>
|
<script src="./index.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -24,16 +23,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
/* global $ */
|
var commands = document.querySelectorAll(".command");
|
||||||
var commands = document.querySelectorAll('.command')
|
Array.prototype.forEach.call(document.querySelectorAll('.command'), function (command) {
|
||||||
Array.prototype.forEach.call(commands, function (command) {
|
var execute = function(){
|
||||||
var execute = function () {
|
eval(command.querySelector("input").value);
|
||||||
// eslint-disable-next-line no-eval
|
|
||||||
eval(command.querySelector('input').value)
|
|
||||||
}
|
}
|
||||||
command.querySelector('button').onclick = execute
|
command.querySelector("button").onclick = execute
|
||||||
$(command.querySelector('input')).keyup(function (e) {
|
$(command.querySelector("input")).keyup(function (e) {
|
||||||
if (e.keyCode === 13) {
|
if (e.keyCode == 13) {
|
||||||
execute()
|
execute()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,13 +1,23 @@
|
|||||||
/* global Y */
|
/* global Y */
|
||||||
|
|
||||||
let y = new Y('xml-example', {
|
// initialize a shared object. This function call returns a promise!
|
||||||
|
Y({
|
||||||
|
db: {
|
||||||
|
name: 'memory'
|
||||||
|
},
|
||||||
connector: {
|
connector: {
|
||||||
name: 'websockets-client',
|
name: 'websockets-client',
|
||||||
url: 'http://127.0.0.1:1234'
|
// url: 'http://127.0.0.1:1234',
|
||||||
|
url: 'http://192.168.178.81:1234',
|
||||||
|
room: 'Xml-example'
|
||||||
|
},
|
||||||
|
sourceDir: '/bower_components',
|
||||||
|
share: {
|
||||||
|
xml: 'Xml("p")' // y.share.xml is of type Y.Xml with tagname "p"
|
||||||
}
|
}
|
||||||
|
}).then(function (y) {
|
||||||
|
window.yXml = y
|
||||||
|
// bind xml type to a dom, and put it in body
|
||||||
|
window.sharedDom = y.share.xml.getDom()
|
||||||
|
document.body.appendChild(window.sharedDom)
|
||||||
})
|
})
|
||||||
|
|
||||||
window.yXml = y
|
|
||||||
// bind xml type to a dom, and put it in body
|
|
||||||
window.sharedDom = y.define('xml', Y.XmlElement).toDom()
|
|
||||||
document.body.appendChild(window.sharedDom)
|
|
||||||
|
|||||||
7
examples/yjs-dist.esm
Normal file
7
examples/yjs-dist.esm
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
|
||||||
|
import Y from '../src/Y.js'
|
||||||
|
import yWebsocketsClient from '../../y-websockets-client/src/y-websockets-client.js'
|
||||||
|
|
||||||
|
Y.extend(yWebsocketsClient)
|
||||||
|
|
||||||
|
export default Y
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
|
|
||||||
/**
|
|
||||||
* Handles named events.
|
|
||||||
*/
|
|
||||||
export default class NamedEventHandler {
|
|
||||||
constructor () {
|
|
||||||
this._eventListener = new Map()
|
|
||||||
this._stateListener = new Map()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
* Returns all listeners that listen to a specified name.
|
|
||||||
*
|
|
||||||
* @param {String} name The query event name.
|
|
||||||
*/
|
|
||||||
_getListener (name) {
|
|
||||||
let listeners = this._eventListener.get(name)
|
|
||||||
if (listeners === undefined) {
|
|
||||||
listeners = {
|
|
||||||
once: new Set(),
|
|
||||||
on: new Set()
|
|
||||||
}
|
|
||||||
this._eventListener.set(name, listeners)
|
|
||||||
}
|
|
||||||
return listeners
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a named event listener. The listener is removed after it has been
|
|
||||||
* called once.
|
|
||||||
*
|
|
||||||
* @param {String} name The event name to listen to.
|
|
||||||
* @param {Function} f The function that is executed when the event is fired.
|
|
||||||
*/
|
|
||||||
once (name, f) {
|
|
||||||
let listeners = this._getListener(name)
|
|
||||||
listeners.once.add(f)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a named event listener.
|
|
||||||
*
|
|
||||||
* @param {String} name The event name to listen to.
|
|
||||||
* @param {Function} f The function that is executed when the event is fired.
|
|
||||||
*/
|
|
||||||
on (name, f) {
|
|
||||||
let listeners = this._getListener(name)
|
|
||||||
listeners.on.add(f)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
* Init the saved state for an event name.
|
|
||||||
*/
|
|
||||||
_initStateListener (name) {
|
|
||||||
let state = this._stateListener.get(name)
|
|
||||||
if (state === undefined) {
|
|
||||||
state = {}
|
|
||||||
state.promise = new Promise(function (resolve) {
|
|
||||||
state.resolve = resolve
|
|
||||||
})
|
|
||||||
this._stateListener.set(name, state)
|
|
||||||
}
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a Promise that is resolved when the event name is called.
|
|
||||||
* The Promise is immediately resolved when the event name was called in the
|
|
||||||
* past.
|
|
||||||
*/
|
|
||||||
when (name) {
|
|
||||||
return this._initStateListener(name).promise
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove an event listener that was registered with either
|
|
||||||
* {@link EventHandler#on} or {@link EventHandler#once}.
|
|
||||||
*/
|
|
||||||
off (name, f) {
|
|
||||||
if (name == null || f == null) {
|
|
||||||
throw new Error('You must specify event name and function!')
|
|
||||||
}
|
|
||||||
const listener = this._eventListener.get(name)
|
|
||||||
if (listener !== undefined) {
|
|
||||||
listener.on.delete(f)
|
|
||||||
listener.once.delete(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Emit a named event. All registered event listeners that listen to the
|
|
||||||
* specified name will receive the event.
|
|
||||||
*
|
|
||||||
* @param {String} name The event name.
|
|
||||||
* @param {Array} args The arguments that are applied to the event listener.
|
|
||||||
*/
|
|
||||||
emit (name, ...args) {
|
|
||||||
this._initStateListener(name).resolve()
|
|
||||||
const listener = this._eventListener.get(name)
|
|
||||||
if (listener !== undefined) {
|
|
||||||
listener.on.forEach(f => f.apply(null, args))
|
|
||||||
listener.once.forEach(f => f.apply(null, args))
|
|
||||||
listener.once = new Set()
|
|
||||||
} else if (name === 'error') {
|
|
||||||
console.error(args[0])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
destroy () {
|
|
||||||
this._eventListener = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
|
|
||||||
export const BITS32 = 0xFFFFFFFF
|
|
||||||
export const BITS21 = (1 << 21) - 1
|
|
||||||
export const BITS16 = (1 << 16) - 1
|
|
||||||
|
|
||||||
export const BIT26 = 1 << 26
|
|
||||||
export const BIT32 = 1 << 32
|
|
||||||
168
lib/decoding.js
168
lib/decoding.js
@@ -1,168 +0,0 @@
|
|||||||
|
|
||||||
/* global Buffer */
|
|
||||||
|
|
||||||
import * as globals from './globals.js'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A Decoder handles the decoding of an ArrayBuffer.
|
|
||||||
*/
|
|
||||||
export class Decoder {
|
|
||||||
/**
|
|
||||||
* @param {ArrayBuffer} buffer Binary data to decode
|
|
||||||
*/
|
|
||||||
constructor (buffer) {
|
|
||||||
this.arr = new Uint8Array(buffer)
|
|
||||||
this.pos = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {ArrayBuffer} buffer
|
|
||||||
* @return {Decoder}
|
|
||||||
*/
|
|
||||||
export const createDecoder = buffer => new Decoder(buffer)
|
|
||||||
|
|
||||||
export const hasContent = decoder => decoder.pos !== decoder.arr.length
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clone a decoder instance.
|
|
||||||
* Optionally set a new position parameter.
|
|
||||||
* @param {Decoder} decoder The decoder instance
|
|
||||||
* @return {Decoder} A clone of `decoder`
|
|
||||||
*/
|
|
||||||
export const clone = (decoder, newPos = decoder.pos) => {
|
|
||||||
let _decoder = createDecoder(decoder.arr.buffer)
|
|
||||||
_decoder.pos = newPos
|
|
||||||
return _decoder
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read `len` bytes as an ArrayBuffer.
|
|
||||||
* @param {Decoder} decoder The decoder instance
|
|
||||||
* @param {number} len The length of bytes to read
|
|
||||||
* @return {ArrayBuffer}
|
|
||||||
*/
|
|
||||||
export const readArrayBuffer = (decoder, len) => {
|
|
||||||
const arrayBuffer = globals.createUint8ArrayFromLen(len)
|
|
||||||
const view = globals.createUint8ArrayFromBuffer(decoder.arr.buffer, decoder.pos, len)
|
|
||||||
arrayBuffer.set(view)
|
|
||||||
decoder.pos += len
|
|
||||||
return arrayBuffer.buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read variable length payload as ArrayBuffer
|
|
||||||
* @param {Decoder} decoder
|
|
||||||
* @return {ArrayBuffer}
|
|
||||||
*/
|
|
||||||
export const readPayload = decoder => readArrayBuffer(decoder, readVarUint(decoder))
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read the rest of the content as an ArrayBuffer
|
|
||||||
* @param {Decoder} decoder
|
|
||||||
* @return {ArrayBuffer}
|
|
||||||
*/
|
|
||||||
export const readTail = decoder => readArrayBuffer(decoder, decoder.arr.length - decoder.pos)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Skip one byte, jump to the next position.
|
|
||||||
* @param {Decoder} decoder The decoder instance
|
|
||||||
* @return {number} The next position
|
|
||||||
*/
|
|
||||||
export const skip8 = decoder => decoder.pos++
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read one byte as unsigned integer.
|
|
||||||
* @param {Decoder} decoder The decoder instance
|
|
||||||
* @return {number} Unsigned 8-bit integer
|
|
||||||
*/
|
|
||||||
export const readUint8 = decoder => decoder.arr[decoder.pos++]
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read 4 bytes as unsigned integer.
|
|
||||||
*
|
|
||||||
* @param {Decoder} decoder
|
|
||||||
* @return {number} An unsigned integer.
|
|
||||||
*/
|
|
||||||
export const readUint32 = decoder => {
|
|
||||||
let uint =
|
|
||||||
decoder.arr[decoder.pos] +
|
|
||||||
(decoder.arr[decoder.pos + 1] << 8) +
|
|
||||||
(decoder.arr[decoder.pos + 2] << 16) +
|
|
||||||
(decoder.arr[decoder.pos + 3] << 24)
|
|
||||||
decoder.pos += 4
|
|
||||||
return uint
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Look ahead without incrementing position.
|
|
||||||
* to the next byte and read it as unsigned integer.
|
|
||||||
*
|
|
||||||
* @param {Decoder} decoder
|
|
||||||
* @return {number} An unsigned integer.
|
|
||||||
*/
|
|
||||||
export const peekUint8 = decoder => decoder.arr[decoder.pos]
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read unsigned integer (32bit) with variable length.
|
|
||||||
* 1/8th of the storage is used as encoding overhead.
|
|
||||||
* * numbers < 2^7 is stored in one bytlength
|
|
||||||
* * numbers < 2^14 is stored in two bylength
|
|
||||||
*
|
|
||||||
* @param {Decoder} decoder
|
|
||||||
* @return {number} An unsigned integer.length
|
|
||||||
*/
|
|
||||||
export const readVarUint = decoder => {
|
|
||||||
let num = 0
|
|
||||||
let len = 0
|
|
||||||
while (true) {
|
|
||||||
let r = decoder.arr[decoder.pos++]
|
|
||||||
num = num | ((r & 0b1111111) << len)
|
|
||||||
len += 7
|
|
||||||
if (r < 1 << 7) {
|
|
||||||
return num >>> 0 // return unsigned number!
|
|
||||||
}
|
|
||||||
if (len > 35) {
|
|
||||||
throw new Error('Integer out of range!')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read string of variable length
|
|
||||||
* * varUint is used to store the length of the string
|
|
||||||
*
|
|
||||||
* Transforming utf8 to a string is pretty expensive. The code performs 10x better
|
|
||||||
* when String.fromCodePoint is fed with all characters as arguments.
|
|
||||||
* But most environments have a maximum number of arguments per functions.
|
|
||||||
* For effiency reasons we apply a maximum of 10000 characters at once.
|
|
||||||
*
|
|
||||||
* @param {Decoder} decoder
|
|
||||||
* @return {String} The read String.
|
|
||||||
*/
|
|
||||||
export const readVarString = decoder => {
|
|
||||||
let remainingLen = readVarUint(decoder)
|
|
||||||
let encodedString = ''
|
|
||||||
while (remainingLen > 0) {
|
|
||||||
const nextLen = remainingLen < 10000 ? remainingLen : 10000
|
|
||||||
const bytes = new Array(nextLen)
|
|
||||||
for (let i = 0; i < nextLen; i++) {
|
|
||||||
bytes[i] = decoder.arr[decoder.pos++]
|
|
||||||
}
|
|
||||||
encodedString += String.fromCodePoint.apply(null, bytes)
|
|
||||||
remainingLen -= nextLen
|
|
||||||
}
|
|
||||||
return decodeURIComponent(escape(encodedString))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Look ahead and read varString without incrementing position
|
|
||||||
* @param {Decoder} decoder
|
|
||||||
* @return {string}
|
|
||||||
*/
|
|
||||||
export const peekVarString = decoder => {
|
|
||||||
let pos = decoder.pos
|
|
||||||
let s = readVarString(decoder)
|
|
||||||
decoder.pos = pos
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
218
lib/encoding.js
218
lib/encoding.js
@@ -1,218 +0,0 @@
|
|||||||
|
|
||||||
import * as globals from './globals.js'
|
|
||||||
|
|
||||||
const bits7 = 0b1111111
|
|
||||||
const bits8 = 0b11111111
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A BinaryEncoder handles the encoding to an ArrayBuffer.
|
|
||||||
*/
|
|
||||||
export class Encoder {
|
|
||||||
constructor () {
|
|
||||||
this.cpos = 0
|
|
||||||
this.cbuf = globals.createUint8ArrayFromLen(1000)
|
|
||||||
this.bufs = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createEncoder = () => new Encoder()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The current length of the encoded data.
|
|
||||||
*/
|
|
||||||
export const length = encoder => {
|
|
||||||
let len = encoder.cpos
|
|
||||||
for (let i = 0; i < encoder.bufs.length; i++) {
|
|
||||||
len += encoder.bufs[i].length
|
|
||||||
}
|
|
||||||
return len
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transform to ArrayBuffer. TODO: rename to .toArrayBuffer
|
|
||||||
* @param {Encoder} encoder
|
|
||||||
* @return {ArrayBuffer} The created ArrayBuffer.
|
|
||||||
*/
|
|
||||||
export const toBuffer = encoder => {
|
|
||||||
const uint8arr = globals.createUint8ArrayFromLen(length(encoder))
|
|
||||||
let curPos = 0
|
|
||||||
for (let i = 0; i < encoder.bufs.length; i++) {
|
|
||||||
let d = encoder.bufs[i]
|
|
||||||
uint8arr.set(d, curPos)
|
|
||||||
curPos += d.length
|
|
||||||
}
|
|
||||||
uint8arr.set(globals.createUint8ArrayFromBuffer(encoder.cbuf.buffer, 0, encoder.cpos), curPos)
|
|
||||||
return uint8arr.buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write one byte to the encoder.
|
|
||||||
*
|
|
||||||
* @param {Encoder} encoder
|
|
||||||
* @param {number} num The byte that is to be encoded.
|
|
||||||
*/
|
|
||||||
export const write = (encoder, num) => {
|
|
||||||
if (encoder.cpos === encoder.cbuf.length) {
|
|
||||||
encoder.bufs.push(encoder.cbuf)
|
|
||||||
encoder.cbuf = globals.createUint8ArrayFromLen(encoder.cbuf.length * 2)
|
|
||||||
encoder.cpos = 0
|
|
||||||
}
|
|
||||||
encoder.cbuf[encoder.cpos++] = num
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write one byte at a specific position.
|
|
||||||
* Position must already be written (i.e. encoder.length > pos)
|
|
||||||
*
|
|
||||||
* @param {Encoder} encoder
|
|
||||||
* @param {number} pos Position to which to write data
|
|
||||||
* @param {number} num Unsigned 8-bit integer
|
|
||||||
*/
|
|
||||||
export const set = (encoder, pos, num) => {
|
|
||||||
let buffer = null
|
|
||||||
// iterate all buffers and adjust position
|
|
||||||
for (let i = 0; i < encoder.bufs.length && buffer === null; i++) {
|
|
||||||
const b = encoder.bufs[i]
|
|
||||||
if (pos < b.length) {
|
|
||||||
buffer = b // found buffer
|
|
||||||
} else {
|
|
||||||
pos -= b.length
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (buffer === null) {
|
|
||||||
// use current buffer
|
|
||||||
buffer = encoder.cbuf
|
|
||||||
}
|
|
||||||
buffer[pos] = num
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write one byte as an unsigned integer.
|
|
||||||
*
|
|
||||||
* @param {Encoder} encoder
|
|
||||||
* @param {number} num The number that is to be encoded.
|
|
||||||
*/
|
|
||||||
export const writeUint8 = (encoder, num) => write(encoder, num & bits8)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write one byte as an unsigned Integer at a specific location.
|
|
||||||
*
|
|
||||||
* @param {Encoder} encoder
|
|
||||||
* @param {number} pos The location where the data will be written.
|
|
||||||
* @param {number} num The number that is to be encoded.
|
|
||||||
*/
|
|
||||||
export const setUint8 = (encoder, pos, num) => set(encoder, pos, num & bits8)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write two bytes as an unsigned integer.
|
|
||||||
*
|
|
||||||
* @param {Encoder} encoder
|
|
||||||
* @param {number} num The number that is to be encoded.
|
|
||||||
*/
|
|
||||||
export const writeUint16 = (encoder, num) => {
|
|
||||||
write(encoder, num & bits8)
|
|
||||||
write(encoder, (num >>> 8) & bits8)
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Write two bytes as an unsigned integer at a specific location.
|
|
||||||
*
|
|
||||||
* @param {Encoder} encoder
|
|
||||||
* @param {number} pos The location where the data will be written.
|
|
||||||
* @param {number} num The number that is to be encoded.
|
|
||||||
*/
|
|
||||||
export const setUint16 = (encoder, pos, num) => {
|
|
||||||
set(encoder, pos, num & bits8)
|
|
||||||
set(encoder, pos + 1, (num >>> 8) & bits8)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write two bytes as an unsigned integer
|
|
||||||
*
|
|
||||||
* @param {Encoder} encoder
|
|
||||||
* @param {number} num The number that is to be encoded.
|
|
||||||
*/
|
|
||||||
export const writeUint32 = (encoder, num) => {
|
|
||||||
for (let i = 0; i < 4; i++) {
|
|
||||||
write(encoder, num & bits8)
|
|
||||||
num >>>= 8
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write two bytes as an unsigned integer at a specific location.
|
|
||||||
*
|
|
||||||
* @param {Encoder} encoder
|
|
||||||
* @param {number} pos The location where the data will be written.
|
|
||||||
* @param {number} num The number that is to be encoded.
|
|
||||||
*/
|
|
||||||
export const setUint32 = (encoder, pos, num) => {
|
|
||||||
for (let i = 0; i < 4; i++) {
|
|
||||||
set(encoder, pos + i, num & bits8)
|
|
||||||
num >>>= 8
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write a variable length unsigned integer.
|
|
||||||
*
|
|
||||||
* Encodes integers in the range from [0, 4294967295] / [0, 0xffffffff]. (max 32 bit unsigned integer)
|
|
||||||
*
|
|
||||||
* @param {Encoder} encoder
|
|
||||||
* @param {number} num The number that is to be encoded.
|
|
||||||
*/
|
|
||||||
export const writeVarUint = (encoder, num) => {
|
|
||||||
while (num >= 0b10000000) {
|
|
||||||
write(encoder, 0b10000000 | (bits7 & num))
|
|
||||||
num >>>= 7
|
|
||||||
}
|
|
||||||
write(encoder, bits7 & num)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write a variable length string.
|
|
||||||
*
|
|
||||||
* @param {Encoder} encoder
|
|
||||||
* @param {String} str The string that is to be encoded.
|
|
||||||
*/
|
|
||||||
export const writeVarString = (encoder, str) => {
|
|
||||||
const encodedString = unescape(encodeURIComponent(str))
|
|
||||||
const len = encodedString.length
|
|
||||||
writeVarUint(encoder, len)
|
|
||||||
for (let i = 0; i < len; i++) {
|
|
||||||
write(encoder, encodedString.codePointAt(i))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write the content of another Encoder.
|
|
||||||
*
|
|
||||||
* TODO: can be improved!
|
|
||||||
*
|
|
||||||
* @param {Encoder} encoder The enUint8Arr
|
|
||||||
* @param {Encoder} append The BinaryEncoder to be written.
|
|
||||||
*/
|
|
||||||
export const writeBinaryEncoder = (encoder, append) => writeArrayBuffer(encoder, toBuffer(append))
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Append an arrayBuffer to the encoder.
|
|
||||||
*
|
|
||||||
* @param {Encoder} encoder
|
|
||||||
* @param {ArrayBuffer} arrayBuffer
|
|
||||||
*/
|
|
||||||
export const writeArrayBuffer = (encoder, arrayBuffer) => {
|
|
||||||
const prevBufferLen = encoder.cbuf.length
|
|
||||||
// TODO: Append to cbuf if possible
|
|
||||||
encoder.bufs.push(globals.createUint8ArrayFromBuffer(encoder.cbuf.buffer, 0, encoder.cpos))
|
|
||||||
encoder.bufs.push(globals.createUint8ArrayFromArrayBuffer(arrayBuffer))
|
|
||||||
encoder.cbuf = globals.createUint8ArrayFromLen(prevBufferLen)
|
|
||||||
encoder.cpos = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Encoder} encoder
|
|
||||||
* @param {ArrayBuffer} arrayBuffer
|
|
||||||
*/
|
|
||||||
export const writePayload = (encoder, arrayBuffer) => {
|
|
||||||
writeVarUint(encoder, arrayBuffer.byteLength)
|
|
||||||
writeArrayBuffer(encoder, arrayBuffer)
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import * as encoding from './encoding.js'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if binary encoding is compatible with golang binary encoding - binary.PutVarUint.
|
|
||||||
*
|
|
||||||
* Result: is compatible up to 32 bit: [0, 4294967295] / [0, 0xffffffff]. (max 32 bit unsigned integer)
|
|
||||||
*/
|
|
||||||
let err = null
|
|
||||||
try {
|
|
||||||
const tests = [
|
|
||||||
{ in: 0, out: [0] },
|
|
||||||
{ in: 1, out: [1] },
|
|
||||||
{ in: 128, out: [128, 1] },
|
|
||||||
{ in: 200, out: [200, 1] },
|
|
||||||
{ in: 32, out: [32] },
|
|
||||||
{ in: 500, out: [244, 3] },
|
|
||||||
{ in: 256, out: [128, 2] },
|
|
||||||
{ in: 700, out: [188, 5] },
|
|
||||||
{ in: 1024, out: [128, 8] },
|
|
||||||
{ in: 1025, out: [129, 8] },
|
|
||||||
{ in: 4048, out: [208, 31] },
|
|
||||||
{ in: 5050, out: [186, 39] },
|
|
||||||
{ in: 1000000, out: [192, 132, 61] },
|
|
||||||
{ in: 34951959, out: [151, 166, 213, 16] },
|
|
||||||
{ in: 2147483646, out: [254, 255, 255, 255, 7] },
|
|
||||||
{ in: 2147483647, out: [255, 255, 255, 255, 7] },
|
|
||||||
{ in: 2147483648, out: [128, 128, 128, 128, 8] },
|
|
||||||
{ in: 2147483700, out: [180, 128, 128, 128, 8] },
|
|
||||||
{ in: 4294967294, out: [254, 255, 255, 255, 15] },
|
|
||||||
{ in: 4294967295, out: [255, 255, 255, 255, 15] }
|
|
||||||
]
|
|
||||||
tests.forEach(test => {
|
|
||||||
const encoder = encoding.createEncoder()
|
|
||||||
encoding.writeVarUint(encoder, test.in)
|
|
||||||
const buffer = new Uint8Array(encoding.toBuffer(encoder))
|
|
||||||
if (buffer.byteLength !== test.out.length) {
|
|
||||||
throw new Error('Length don\'t match!')
|
|
||||||
}
|
|
||||||
for (let j = 0; j < buffer.length; j++) {
|
|
||||||
if (buffer[j] !== test[1][j]) {
|
|
||||||
throw new Error('values don\'t match!')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
err = error
|
|
||||||
} finally {
|
|
||||||
console.log('YDB Client: Encoding varUint compatiblity test: ', err || 'success!')
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
/* eslint-env browser */
|
|
||||||
|
|
||||||
export const Uint8Array_ = Uint8Array
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Array<number>} arr
|
|
||||||
* @return {ArrayBuffer}
|
|
||||||
*/
|
|
||||||
export const createArrayBufferFromArray = arr => new Uint8Array_(arr).buffer
|
|
||||||
|
|
||||||
export const createUint8ArrayFromLen = len => new Uint8Array_(len)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create Uint8Array with initial content from buffer
|
|
||||||
*/
|
|
||||||
export const createUint8ArrayFromBuffer = (buffer, byteOffset, length) => new Uint8Array_(buffer, byteOffset, length)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create Uint8Array with initial content from buffer
|
|
||||||
*/
|
|
||||||
export const createUint8ArrayFromArrayBuffer = arraybuffer => new Uint8Array_(arraybuffer)
|
|
||||||
export const createArrayFromArrayBuffer = arraybuffer => Array.from(createUint8ArrayFromArrayBuffer(arraybuffer))
|
|
||||||
|
|
||||||
export const createPromise = f => new Promise(f)
|
|
||||||
|
|
||||||
export const createMap = () => new Map()
|
|
||||||
export const createSet = () => new Set()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* `Promise.all` wait for all promises in the array to resolve and return the result
|
|
||||||
* @param {Array<Promise<any>>} arrp
|
|
||||||
* @return {any}
|
|
||||||
*/
|
|
||||||
export const pall = arrp => Promise.all(arrp)
|
|
||||||
export const preject = reason => Promise.reject(reason)
|
|
||||||
export const presolve = res => Promise.resolve(res)
|
|
||||||
|
|
||||||
export const until = (timeout, check) => createPromise((resolve, reject) => {
|
|
||||||
const hasTimeout = timeout > 0
|
|
||||||
const untilInterval = () => {
|
|
||||||
if (check()) {
|
|
||||||
clearInterval(intervalHandle)
|
|
||||||
resolve()
|
|
||||||
} else if (hasTimeout) {
|
|
||||||
timeout -= 10
|
|
||||||
if (timeout < 0) {
|
|
||||||
clearInterval(intervalHandle)
|
|
||||||
reject(error('Timeout'))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const intervalHandle = setInterval(untilInterval, 10)
|
|
||||||
})
|
|
||||||
|
|
||||||
export const error = description => new Error(description)
|
|
||||||
|
|
||||||
export const max = (a, b) => a > b ? a : b
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {number} t Time to wait
|
|
||||||
* @return {Promise} Promise that is resolved after t ms
|
|
||||||
*/
|
|
||||||
export const wait = t => createPromise(r => setTimeout(r, t))
|
|
||||||
159
lib/idb.js
159
lib/idb.js
@@ -1,159 +0,0 @@
|
|||||||
/* eslint-env browser */
|
|
||||||
|
|
||||||
import * as globals from './globals.js'
|
|
||||||
|
|
||||||
/*
|
|
||||||
* IDB Request to Promise transformer
|
|
||||||
*/
|
|
||||||
export const rtop = request => globals.createPromise((resolve, reject) => {
|
|
||||||
request.onerror = event => reject(new Error(event.target.error))
|
|
||||||
request.onblocked = () => location.reload()
|
|
||||||
request.onsuccess = event => resolve(event.target.result)
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return {Promise<IDBDatabase>}
|
|
||||||
*/
|
|
||||||
export const openDB = (name, initDB) => globals.createPromise((resolve, reject) => {
|
|
||||||
let request = indexedDB.open(name)
|
|
||||||
/**
|
|
||||||
* @param {any} event
|
|
||||||
*/
|
|
||||||
request.onupgradeneeded = event => initDB(event.target.result)
|
|
||||||
/**
|
|
||||||
* @param {any} event
|
|
||||||
*/
|
|
||||||
request.onerror = event => reject(new Error(event.target.error))
|
|
||||||
request.onblocked = () => location.reload()
|
|
||||||
/**
|
|
||||||
* @param {any} event
|
|
||||||
*/
|
|
||||||
request.onsuccess = event => {
|
|
||||||
const db = event.target.result
|
|
||||||
db.onversionchange = () => { db.close() }
|
|
||||||
addEventListener('unload', () => db.close())
|
|
||||||
resolve(db)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export const deleteDB = name => rtop(indexedDB.deleteDatabase(name))
|
|
||||||
|
|
||||||
export const createStores = (db, definitions) => definitions.forEach(d =>
|
|
||||||
db.createObjectStore.apply(db, d)
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {IDBObjectStore} store
|
|
||||||
* @param {String | number | ArrayBuffer | Date | Array } key
|
|
||||||
* @return {Promise<ArrayBuffer>}
|
|
||||||
*/
|
|
||||||
export const get = (store, key) =>
|
|
||||||
rtop(store.get(key))
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {IDBObjectStore} store
|
|
||||||
* @param {String | number | ArrayBuffer | Date | IDBKeyRange | Array } key
|
|
||||||
*/
|
|
||||||
export const del = (store, key) =>
|
|
||||||
rtop(store.delete(key))
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {IDBObjectStore} store
|
|
||||||
* @param {String | number | ArrayBuffer | Date | boolean} item
|
|
||||||
* @param {String | number | ArrayBuffer | Date | Array} [key]
|
|
||||||
*/
|
|
||||||
export const put = (store, item, key) =>
|
|
||||||
rtop(store.put(item, key))
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {IDBObjectStore} store
|
|
||||||
* @param {String | number | ArrayBuffer | Date | boolean} item
|
|
||||||
* @param {String | number | ArrayBuffer | Date | Array} [key]
|
|
||||||
* @return {Promise<ArrayBuffer>}
|
|
||||||
*/
|
|
||||||
export const add = (store, item, key) =>
|
|
||||||
rtop(store.add(item, key))
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {IDBObjectStore} store
|
|
||||||
* @param {String | number | ArrayBuffer | Date} item
|
|
||||||
* @return {Promise<number>}
|
|
||||||
*/
|
|
||||||
export const addAutoKey = (store, item) =>
|
|
||||||
rtop(store.add(item))
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {IDBObjectStore} store
|
|
||||||
* @param {IDBKeyRange} [range]
|
|
||||||
*/
|
|
||||||
export const getAll = (store, range) =>
|
|
||||||
rtop(store.getAll(range))
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {IDBObjectStore} store
|
|
||||||
* @param {IDBKeyRange} [range]
|
|
||||||
*/
|
|
||||||
export const getAllKeys = (store, range) =>
|
|
||||||
rtop(store.getAllKeys(range))
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef KeyValuePair
|
|
||||||
* @type {Object}
|
|
||||||
* @property {any} k key
|
|
||||||
* @property {any} v Value
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {IDBObjectStore} store
|
|
||||||
* @param {IDBKeyRange} [range]
|
|
||||||
* @return {Promise<Array<KeyValuePair>>}
|
|
||||||
*/
|
|
||||||
export const getAllKeysValues = (store, range) =>
|
|
||||||
globals.pall([getAllKeys(store, range), getAll(store, range)]).then(([ks, vs]) => ks.map((k, i) => ({ k, v: vs[i] })))
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Iterate on keys and values
|
|
||||||
* @param {IDBObjectStore} store
|
|
||||||
* @param {IDBKeyRange?} keyrange
|
|
||||||
* @param {function(any, any)} f Return true in order to continue the cursor
|
|
||||||
*/
|
|
||||||
export const iterate = (store, keyrange, f) => globals.createPromise((resolve, reject) => {
|
|
||||||
const request = store.openCursor(keyrange)
|
|
||||||
request.onerror = reject
|
|
||||||
/**
|
|
||||||
* @param {any} event
|
|
||||||
*/
|
|
||||||
request.onsuccess = event => {
|
|
||||||
const cursor = event.target.result
|
|
||||||
if (cursor === null) {
|
|
||||||
return resolve()
|
|
||||||
}
|
|
||||||
f(cursor.value, cursor.key)
|
|
||||||
cursor.continue()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Iterate on the keys (no values)
|
|
||||||
* @param {IDBObjectStore} store
|
|
||||||
* @param {IDBKeyRange} keyrange
|
|
||||||
* @param {function(IDBCursor)} f Call `idbcursor.continue()` to iterate further
|
|
||||||
*/
|
|
||||||
export const iterateKeys = (store, keyrange, f) => {
|
|
||||||
/**
|
|
||||||
* @param {any} event
|
|
||||||
*/
|
|
||||||
store.openKeyCursor(keyrange).onsuccess = event => f(event.target.result)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Open store from transaction
|
|
||||||
* @param {IDBTransaction} t
|
|
||||||
* @param {String} store
|
|
||||||
* @returns {IDBObjectStore}
|
|
||||||
*/
|
|
||||||
export const getStore = (t, store) => t.objectStore(store)
|
|
||||||
|
|
||||||
export const createIDBKeyRangeBound = (lower, upper, lowerOpen, upperOpen) => IDBKeyRange.bound(lower, upper, lowerOpen, upperOpen)
|
|
||||||
export const createIDBKeyRangeUpperBound = (upper, upperOpen) => IDBKeyRange.upperBound(upper, upperOpen)
|
|
||||||
export const createIDBKeyRangeLowerBound = (lower, lowerOpen) => IDBKeyRange.lowerBound(lower, lowerOpen)
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import * as test from './test.js'
|
|
||||||
import * as idb from './idb.js'
|
|
||||||
import * as logging from './logging.js'
|
|
||||||
|
|
||||||
const initTestDB = db => idb.createStores(db, [['test']])
|
|
||||||
const testDBName = 'idb-test'
|
|
||||||
|
|
||||||
const createTransaction = db => db.transaction(['test'], 'readwrite')
|
|
||||||
/**
|
|
||||||
* @param {IDBTransaction} t
|
|
||||||
* @return {IDBObjectStore}
|
|
||||||
*/
|
|
||||||
const getStore = t => idb.getStore(t, 'test')
|
|
||||||
|
|
||||||
idb.deleteDB(testDBName).then(() => idb.openDB(testDBName, initTestDB)).then(db => {
|
|
||||||
test.run('idb iteration', async testname => {
|
|
||||||
const t = createTransaction(db)
|
|
||||||
await idb.put(getStore(t), 0, ['t', 0])
|
|
||||||
await idb.put(getStore(t), 1, ['t', 1])
|
|
||||||
const valsGetAll = await idb.getAll(getStore(t))
|
|
||||||
if (valsGetAll.length !== 2) {
|
|
||||||
logging.fail('getAll does not return two values')
|
|
||||||
}
|
|
||||||
const valsIterate = []
|
|
||||||
const keyrange = idb.createIDBKeyRangeBound(['t', 0], ['t', 1], false, false)
|
|
||||||
await idb.put(getStore(t), 2, ['t', 2])
|
|
||||||
await idb.iterate(getStore(t), keyrange, (val, key) => {
|
|
||||||
valsIterate.push(val)
|
|
||||||
})
|
|
||||||
if (valsIterate.length !== 2) {
|
|
||||||
logging.fail('iterate does not return two values')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
|
|
||||||
import * as globals from './globals.js'
|
|
||||||
|
|
||||||
let date = new Date().getTime()
|
|
||||||
|
|
||||||
const writeDate = () => {
|
|
||||||
const oldDate = date
|
|
||||||
date = new Date().getTime()
|
|
||||||
return date - oldDate
|
|
||||||
}
|
|
||||||
|
|
||||||
export const print = (...args) => console.log(...args)
|
|
||||||
export const log = m => print(`%cydb-client: %c${m} %c+${writeDate()}ms`, 'color: blue;', '', 'color: blue')
|
|
||||||
|
|
||||||
export const fail = m => {
|
|
||||||
throw new Error(m)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {ArrayBuffer} buffer
|
|
||||||
* @return {string}
|
|
||||||
*/
|
|
||||||
export const arrayBufferToString = buffer => JSON.stringify(Array.from(globals.createUint8ArrayFromBuffer(buffer)))
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
|
|
||||||
export const floor = Math.floor
|
|
||||||
32
lib/mutex.js
32
lib/mutex.js
@@ -1,32 +0,0 @@
|
|||||||
|
|
||||||
/**
|
|
||||||
* Creates a mutual exclude function with the following property:
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* const mutex = createMutex()
|
|
||||||
* mutex(function () {
|
|
||||||
* // This function is immediately executed
|
|
||||||
* mutex(function () {
|
|
||||||
* // This function is never executed, as it is called with the same
|
|
||||||
* // mutex function
|
|
||||||
* })
|
|
||||||
* })
|
|
||||||
*
|
|
||||||
* @return {Function} A mutual exclude function
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
export const createMutex = () => {
|
|
||||||
let token = true
|
|
||||||
return (f, g) => {
|
|
||||||
if (token) {
|
|
||||||
token = false
|
|
||||||
try {
|
|
||||||
f()
|
|
||||||
} finally {
|
|
||||||
token = true
|
|
||||||
}
|
|
||||||
} else if (g !== undefined) {
|
|
||||||
g()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER
|
|
||||||
export const MIN_SAFE_INTEGER = Number.MIN_SAFE_INTEGER
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
const N = 624
|
|
||||||
const M = 397
|
|
||||||
|
|
||||||
function twist (u, v) {
|
|
||||||
return ((((u & 0x80000000) | (v & 0x7fffffff)) >>> 1) ^ ((v & 1) ? 0x9908b0df : 0))
|
|
||||||
}
|
|
||||||
|
|
||||||
function nextState (state) {
|
|
||||||
let p = 0
|
|
||||||
let j
|
|
||||||
for (j = N - M + 1; --j; p++) {
|
|
||||||
state[p] = state[p + M] ^ twist(state[p], state[p + 1])
|
|
||||||
}
|
|
||||||
for (j = M; --j; p++) {
|
|
||||||
state[p] = state[p + M - N] ^ twist(state[p], state[p + 1])
|
|
||||||
}
|
|
||||||
state[p] = state[p + M - N] ^ twist(state[p], state[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a port of Shawn Cokus's implementation of the original Mersenne Twister algorithm (http://www.math.sci.hiroshima-u.ac.jp/~m-mat/MT/MT2002/CODES/MTARCOK/mt19937ar-cok.c).
|
|
||||||
* MT has a very high period of 2^19937. Though the authors of xorshift describe that a high period is not
|
|
||||||
* very relevant (http://vigna.di.unimi.it/xorshift/). It is four times slower than xoroshiro128plus and
|
|
||||||
* needs to recompute its state after generating 624 numbers.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* const gen = new Mt19937(new Date().getTime())
|
|
||||||
* console.log(gen.next())
|
|
||||||
*
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
export default class Mt19937 {
|
|
||||||
/**
|
|
||||||
* @param {Number} seed The starting point for the random number generation. If you use the same seed, the generator will return the same sequence of random numbers.
|
|
||||||
*/
|
|
||||||
constructor (seed) {
|
|
||||||
this.seed = seed
|
|
||||||
const state = new Uint32Array(N)
|
|
||||||
state[0] = seed
|
|
||||||
for (let i = 1; i < N; i++) {
|
|
||||||
state[i] = (Math.imul(1812433253, (state[i - 1] ^ (state[i - 1] >>> 30))) + i) & 0xFFFFFFFF
|
|
||||||
}
|
|
||||||
this._state = state
|
|
||||||
this._i = 0
|
|
||||||
nextState(this._state)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate a random signed integer.
|
|
||||||
*
|
|
||||||
* @return {Number} A 32 bit signed integer.
|
|
||||||
*/
|
|
||||||
next () {
|
|
||||||
if (this._i === N) {
|
|
||||||
// need to compute a new state
|
|
||||||
nextState(this._state)
|
|
||||||
this._i = 0
|
|
||||||
}
|
|
||||||
let y = this._state[this._i++]
|
|
||||||
y ^= (y >>> 11)
|
|
||||||
y ^= (y << 7) & 0x9d2c5680
|
|
||||||
y ^= (y << 15) & 0xefc60000
|
|
||||||
y ^= (y >>> 18)
|
|
||||||
return y
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
|
|
||||||
import Mt19937 from './Mt19937.js'
|
|
||||||
import Xoroshiro128plus from './Xoroshiro128plus.js'
|
|
||||||
import Xorshift32 from './Xorshift32.js'
|
|
||||||
import * as time from '../../time.js'
|
|
||||||
|
|
||||||
const DIAMETER = 300
|
|
||||||
const NUMBERS = 10000
|
|
||||||
|
|
||||||
function runPRNG (name, Gen) {
|
|
||||||
console.log('== ' + name + ' ==')
|
|
||||||
const gen = new Gen(1234)
|
|
||||||
let head = 0
|
|
||||||
let tails = 0
|
|
||||||
const date = time.getUnixTime()
|
|
||||||
const canvas = document.createElement('canvas')
|
|
||||||
canvas.height = DIAMETER
|
|
||||||
canvas.width = DIAMETER
|
|
||||||
const ctx = canvas.getContext('2d')
|
|
||||||
const vals = new Set()
|
|
||||||
ctx.fillStyle = 'blue'
|
|
||||||
for (let i = 0; i < NUMBERS; i++) {
|
|
||||||
const n = gen.next() & 0xFFFFFF
|
|
||||||
const x = (gen.next() >>> 0) % DIAMETER
|
|
||||||
const y = (gen.next() >>> 0) % DIAMETER
|
|
||||||
ctx.fillRect(x, y, 1, 2)
|
|
||||||
if ((n & 1) === 1) {
|
|
||||||
head++
|
|
||||||
} else {
|
|
||||||
tails++
|
|
||||||
}
|
|
||||||
if (vals.has(n)) {
|
|
||||||
console.warn(`The generator generated a duplicate`)
|
|
||||||
}
|
|
||||||
vals.add(n)
|
|
||||||
}
|
|
||||||
console.log('time: ', time.getUnixTime() - date)
|
|
||||||
console.log('head:', head, 'tails:', tails)
|
|
||||||
console.log('%c ', `font-size: 200px; background: url(${canvas.toDataURL()}) no-repeat;`)
|
|
||||||
const h1 = document.createElement('h1')
|
|
||||||
h1.insertBefore(document.createTextNode(name), null)
|
|
||||||
document.body.insertBefore(h1, null)
|
|
||||||
document.body.appendChild(canvas)
|
|
||||||
}
|
|
||||||
|
|
||||||
runPRNG('mt19937', Mt19937)
|
|
||||||
runPRNG('xoroshiro128plus', Xoroshiro128plus)
|
|
||||||
runPRNG('xorshift32', Xorshift32)
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
# Pseudo Random Number Generators (PRNG)
|
|
||||||
|
|
||||||
Given a seed a PRNG generates a sequence of numbers that cannot be reasonably predicted. Two PRNGs must generate the same random sequence of numbers if given the same seed.
|
|
||||||
|
|
||||||
TODO: explain what POINT is
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
|
|
||||||
import Xorshift32 from './Xorshift32.js'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a variant of xoroshiro128plus - the fastest full-period generator passing BigCrush without systematic failures.
|
|
||||||
*
|
|
||||||
* This implementation follows the idea of the original xoroshiro128plus implementation,
|
|
||||||
* but is optimized for the JavaScript runtime. I.e.
|
|
||||||
* * The operations are performed on 32bit integers (the original implementation works with 64bit values).
|
|
||||||
* * The initial 128bit state is computed based on a 32bit seed and Xorshift32.
|
|
||||||
* * This implementation returns two 32bit values based on the 64bit value that is computed by xoroshiro128plus.
|
|
||||||
* Caution: The last addition step works slightly different than in the original implementation - the add carry of the
|
|
||||||
* first 32bit addition is not carried over to the last 32bit.
|
|
||||||
*
|
|
||||||
* [Reference implementation](http://vigna.di.unimi.it/xorshift/xoroshiro128plus.c)
|
|
||||||
*/
|
|
||||||
export default class Xoroshiro128plus {
|
|
||||||
constructor (seed) {
|
|
||||||
this.seed = seed
|
|
||||||
// This is a variant of Xoroshiro128plus to fill the initial state
|
|
||||||
const xorshift32 = new Xorshift32(seed)
|
|
||||||
this.state = new Uint32Array(4)
|
|
||||||
for (let i = 0; i < 4; i++) {
|
|
||||||
this.state[i] = xorshift32.next()
|
|
||||||
}
|
|
||||||
this._fresh = true
|
|
||||||
}
|
|
||||||
next () {
|
|
||||||
const state = this.state
|
|
||||||
if (this._fresh) {
|
|
||||||
this._fresh = false
|
|
||||||
return (state[0] + state[2]) & 0xFFFFFFFF
|
|
||||||
} else {
|
|
||||||
this._fresh = true
|
|
||||||
const s0 = state[0]
|
|
||||||
const s1 = state[1]
|
|
||||||
const s2 = state[2] ^ s0
|
|
||||||
const s3 = state[3] ^ s1
|
|
||||||
// function js_rotl (x, k) {
|
|
||||||
// k = k - 32
|
|
||||||
// const x1 = x[0]
|
|
||||||
// const x2 = x[1]
|
|
||||||
// x[0] = x2 << k | x1 >>> (32 - k)
|
|
||||||
// x[1] = x1 << k | x2 >>> (32 - k)
|
|
||||||
// }
|
|
||||||
// rotl(s0, 55) // k = 23 = 55 - 32; j = 9 = 32 - 23
|
|
||||||
state[0] = (s1 << 23 | s0 >>> 9) ^ s2 ^ (s2 << 14 | s3 >>> 18)
|
|
||||||
state[1] = (s0 << 23 | s1 >>> 9) ^ s3 ^ (s3 << 14)
|
|
||||||
// rol(s1, 36) // k = 4 = 36 - 32; j = 23 = 32 - 9
|
|
||||||
state[2] = s3 << 4 | s2 >>> 28
|
|
||||||
state[3] = s2 << 4 | s3 >>> 28
|
|
||||||
return (state[1] + state[3]) & 0xFFFFFFFF
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
// reference implementation
|
|
||||||
#include <stdint.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
|
|
||||||
uint64_t s[2];
|
|
||||||
|
|
||||||
static inline uint64_t rotl(const uint64_t x, int k) {
|
|
||||||
return (x << k) | (x >> (64 - k));
|
|
||||||
}
|
|
||||||
|
|
||||||
uint64_t next(void) {
|
|
||||||
const uint64_t s0 = s[0];
|
|
||||||
uint64_t s1 = s[1];
|
|
||||||
s1 ^= s0;
|
|
||||||
s[0] = rotl(s0, 55) ^ s1 ^ (s1 << 14); // a, b
|
|
||||||
s[1] = rotl(s1, 36); // c
|
|
||||||
return (s[0] + s[1]) & 0xFFFFFFFF;
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(void)
|
|
||||||
{
|
|
||||||
int i;
|
|
||||||
s[0] = 1111 | (1337ul << 32);
|
|
||||||
s[1] = 1234 | (9999ul << 32);
|
|
||||||
|
|
||||||
printf("1000 outputs of genrand_int31()\n");
|
|
||||||
for (i=0; i<100; i++) {
|
|
||||||
printf("%10lu ", i);
|
|
||||||
printf("%10lu ", next());
|
|
||||||
printf("- %10lu ", s[0] >> 32);
|
|
||||||
printf("%10lu ", (s[0] << 32) >> 32);
|
|
||||||
printf("%10lu ", s[1] >> 32);
|
|
||||||
printf("%10lu ", (s[1] << 32) >> 32);
|
|
||||||
printf("\n");
|
|
||||||
// if (i%5==4) printf("\n");
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
*/
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
|
|
||||||
/**
|
|
||||||
* Xorshift32 is a very simple but elegang PRNG with a period of `2^32-1`.
|
|
||||||
*/
|
|
||||||
export default class Xorshift32 {
|
|
||||||
/**
|
|
||||||
* @param {number} seed The starting point for the random number generation. If you use the same seed, the generator will return the same sequence of random numbers.
|
|
||||||
*/
|
|
||||||
constructor (seed) {
|
|
||||||
this.seed = seed
|
|
||||||
this._state = seed
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Generate a random signed integer.
|
|
||||||
*
|
|
||||||
* @return {Number} A 32 bit signed integer.
|
|
||||||
*/
|
|
||||||
next () {
|
|
||||||
let x = this._state
|
|
||||||
x ^= x << 13
|
|
||||||
x ^= x >> 17
|
|
||||||
x ^= x << 5
|
|
||||||
this._state = x
|
|
||||||
return x
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
|
|
||||||
import * as binary from '../binary.js'
|
|
||||||
import { fromCharCode, fromCodePoint } from '../string.js'
|
|
||||||
import { MAX_SAFE_INTEGER, MIN_SAFE_INTEGER } from '../number.js'
|
|
||||||
import * as math from '../math.js'
|
|
||||||
|
|
||||||
import DefaultPRNG from './PRNG/Xoroshiro128plus.js'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Description of the function
|
|
||||||
* @callback generatorNext
|
|
||||||
* @return {number} A 32bit integer
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A random type generator.
|
|
||||||
*
|
|
||||||
* @typedef {Object} PRNG
|
|
||||||
* @property {generatorNext} next Generate new number
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a Xoroshiro128plus Pseudo-Random-Number-Generator.
|
|
||||||
* This is the fastest full-period generator passing BigCrush without systematic failures.
|
|
||||||
* But there are more PRNGs available in ./PRNG/.
|
|
||||||
*
|
|
||||||
* @param {number} seed A positive 32bit integer. Do not use negative numbers.
|
|
||||||
* @return {PRNG}
|
|
||||||
*/
|
|
||||||
export const createPRNG = seed => new DefaultPRNG(Math.floor(seed < 1 ? seed * binary.BITS32 : seed))
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates a single random bool.
|
|
||||||
*
|
|
||||||
* @param {PRNG} gen A random number generator.
|
|
||||||
* @return {Boolean} A random boolean
|
|
||||||
*/
|
|
||||||
export const bool = gen => (gen.next() & 2) === 2 // brackets are non-optional!
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates a random integer with 53 bit resolution.
|
|
||||||
*
|
|
||||||
* @param {PRNG} gen A random number generator.
|
|
||||||
* @param {Number} [min = MIN_SAFE_INTEGER] The lower bound of the allowed return values (inclusive).
|
|
||||||
* @param {Number} [max = MAX_SAFE_INTEGER] The upper bound of the allowed return values (inclusive).
|
|
||||||
* @return {Number} A random integer on [min, max]
|
|
||||||
*/
|
|
||||||
export const int53 = (gen, min = MIN_SAFE_INTEGER, max = MAX_SAFE_INTEGER) => math.floor(real53(gen) * (max + 1 - min) + min)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates a random integer with 32 bit resolution.
|
|
||||||
*
|
|
||||||
* @param {PRNG} gen A random number generator.
|
|
||||||
* @param {Number} [min = MIN_SAFE_INTEGER] The lower bound of the allowed return values (inclusive).
|
|
||||||
* @param {Number} [max = MAX_SAFE_INTEGER] The upper bound of the allowed return values (inclusive).
|
|
||||||
* @return {Number} A random integer on [min, max]
|
|
||||||
*/
|
|
||||||
export const int32 = (gen, min = MIN_SAFE_INTEGER, max = MAX_SAFE_INTEGER) => min + ((gen.next() >>> 0) % (max + 1 - min))
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates a random real on [0, 1) with 32 bit resolution.
|
|
||||||
*
|
|
||||||
* @param {PRNG} gen A random number generator.
|
|
||||||
* @return {Number} A random real number on [0, 1).
|
|
||||||
*/
|
|
||||||
export const real32 = gen => (gen.next() >>> 0) / binary.BITS32
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates a random real on [0, 1) with 53 bit resolution.
|
|
||||||
*
|
|
||||||
* @param {PRNG} gen A random number generator.
|
|
||||||
* @return {Number} A random real number on [0, 1).
|
|
||||||
*/
|
|
||||||
export const real53 = gen => (((gen.next() >>> 5) * binary.BIT26) + (gen.next() >>> 6)) / MAX_SAFE_INTEGER
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates a random character from char code 32 - 126. I.e. Characters, Numbers, special characters, and Space:
|
|
||||||
*
|
|
||||||
* (Space)!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[/]^_`abcdefghijklmnopqrstuvwxyz{|}~
|
|
||||||
*/
|
|
||||||
export const char = gen => fromCharCode(int32(gen, 32, 126))
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {PRNG} gen
|
|
||||||
* @return {string} A single letter (a-z)
|
|
||||||
*/
|
|
||||||
export const letter = gen => fromCharCode(int32(gen, 97, 122))
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {PRNG} gen
|
|
||||||
* @return {string} A random word without spaces consisting of letters (a-z)
|
|
||||||
*/
|
|
||||||
export const word = gen => {
|
|
||||||
const len = int32(gen, 0, 20)
|
|
||||||
let str = ''
|
|
||||||
for (let i = 0; i < len; i++) {
|
|
||||||
str += letter(gen)
|
|
||||||
}
|
|
||||||
return str
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO: this function produces invalid runes. Does not cover all of utf16!!
|
|
||||||
*/
|
|
||||||
export const utf16Rune = gen => {
|
|
||||||
const codepoint = int32(gen, 0, 256)
|
|
||||||
return fromCodePoint(codepoint)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {PRNG} gen
|
|
||||||
* @param {number} [maxlen = 20]
|
|
||||||
*/
|
|
||||||
export const utf16String = (gen, maxlen = 20) => {
|
|
||||||
const len = int32(gen, 0, maxlen)
|
|
||||||
let str = ''
|
|
||||||
for (let i = 0; i < len; i++) {
|
|
||||||
str += utf16Rune(gen)
|
|
||||||
}
|
|
||||||
return str
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns one element of a given array.
|
|
||||||
*
|
|
||||||
* @param {PRNG} gen A random number generator.
|
|
||||||
* @param {Array<T>} array Non empty Array of possible values.
|
|
||||||
* @return {T} One of the values of the supplied Array.
|
|
||||||
* @template T
|
|
||||||
*/
|
|
||||||
export const oneOf = (gen, array) => array[int32(gen, 0, array.length - 1)]
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
/**
|
|
||||||
*TODO: enable tests
|
|
||||||
import * as rt from '../rich-text/formatters.mjs'
|
|
||||||
import { test } from '../test/test.mjs'
|
|
||||||
import Xoroshiro128plus from './PRNG/Xoroshiro128plus.mjs'
|
|
||||||
import Xorshift32 from './PRNG/Xorshift32.mjs'
|
|
||||||
import MT19937 from './PRNG/Mt19937.mjs'
|
|
||||||
import { generateBool, generateInt, generateInt32, generateReal, generateChar } from './random.mjs'
|
|
||||||
import { MAX_SAFE_INTEGER } from '../number/constants.mjs'
|
|
||||||
import { BIT32 } from '../binary/constants.mjs'
|
|
||||||
|
|
||||||
function init (Gen) {
|
|
||||||
return {
|
|
||||||
gen: new Gen(1234)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const PRNGs = [
|
|
||||||
{ name: 'Xoroshiro128plus', Gen: Xoroshiro128plus },
|
|
||||||
{ name: 'Xorshift32', Gen: Xorshift32 },
|
|
||||||
{ name: 'MT19937', Gen: MT19937 }
|
|
||||||
]
|
|
||||||
|
|
||||||
const ITERATONS = 1000000
|
|
||||||
|
|
||||||
for (const PRNG of PRNGs) {
|
|
||||||
const prefix = rt.orange`${PRNG.name}:`
|
|
||||||
|
|
||||||
test(rt.plain`${prefix} generateBool`, function generateBoolTest (t) {
|
|
||||||
const { gen } = init(PRNG.Gen)
|
|
||||||
let head = 0
|
|
||||||
let tail = 0
|
|
||||||
let b
|
|
||||||
let i
|
|
||||||
|
|
||||||
for (i = 0; i < ITERATONS; i++) {
|
|
||||||
b = generateBool(gen)
|
|
||||||
if (b) {
|
|
||||||
head++
|
|
||||||
} else {
|
|
||||||
tail++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
t.log(`Generated ${head} heads and ${tail} tails.`)
|
|
||||||
t.assert(tail >= Math.floor(ITERATONS * 0.49), 'Generated enough tails.')
|
|
||||||
t.assert(head >= Math.floor(ITERATONS * 0.49), 'Generated enough heads.')
|
|
||||||
})
|
|
||||||
|
|
||||||
test(rt.plain`${prefix} generateInt integers average correctly`, function averageIntTest (t) {
|
|
||||||
const { gen } = init(PRNG.Gen)
|
|
||||||
let count = 0
|
|
||||||
let i
|
|
||||||
|
|
||||||
for (i = 0; i < ITERATONS; i++) {
|
|
||||||
count += generateInt(gen, 0, 100)
|
|
||||||
}
|
|
||||||
const average = count / ITERATONS
|
|
||||||
const expectedAverage = 100 / 2
|
|
||||||
t.log(`Average is: ${average}. Expected average is ${expectedAverage}.`)
|
|
||||||
t.assert(Math.abs(average - expectedAverage) <= 1, 'Expected average is at most 1 off.')
|
|
||||||
})
|
|
||||||
|
|
||||||
test(rt.plain`${prefix} generateInt32 generates integer with 32 bits`, function generateLargeIntegers (t) {
|
|
||||||
const { gen } = init(PRNG.Gen)
|
|
||||||
let num = 0
|
|
||||||
let i
|
|
||||||
let newNum
|
|
||||||
for (i = 0; i < ITERATONS; i++) {
|
|
||||||
newNum = generateInt32(gen, 0, MAX_SAFE_INTEGER)
|
|
||||||
if (newNum > num) {
|
|
||||||
num = newNum
|
|
||||||
}
|
|
||||||
}
|
|
||||||
t.log(`Largest number generated is ${num} (0b${num.toString(2)})`)
|
|
||||||
t.assert(num > (BIT32 >>> 0), 'Largest number is 32 bits long.')
|
|
||||||
})
|
|
||||||
|
|
||||||
test(rt.plain`${prefix} generateReal has 53 bit resolution`, function real53bitResolution (t) {
|
|
||||||
const { gen } = init(PRNG.Gen)
|
|
||||||
let num = 0
|
|
||||||
let i
|
|
||||||
let newNum
|
|
||||||
for (i = 0; i < ITERATONS; i++) {
|
|
||||||
newNum = generateReal(gen) * MAX_SAFE_INTEGER
|
|
||||||
if (newNum > num) {
|
|
||||||
num = newNum
|
|
||||||
}
|
|
||||||
}
|
|
||||||
t.log(`Largest number generated is ${num}.`)
|
|
||||||
t.assert((MAX_SAFE_INTEGER - num) / MAX_SAFE_INTEGER < 0.01, 'Largest number is close to MAX_SAFE_INTEGER (at most 1% off).')
|
|
||||||
})
|
|
||||||
|
|
||||||
test(rt.plain`${prefix} generateChar generates all described characters`, function real53bitResolution (t) {
|
|
||||||
const { gen } = init(PRNG.Gen)
|
|
||||||
const charSet = new Set()
|
|
||||||
const chars = ' !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[/]^_`abcdefghijklmnopqrstuvwxyz{|}~"'
|
|
||||||
let i
|
|
||||||
let char
|
|
||||||
for (i = chars.length - 1; i >= 0; i--) {
|
|
||||||
charSet.add(chars[i])
|
|
||||||
}
|
|
||||||
for (i = 0; i < ITERATONS; i++) {
|
|
||||||
char = generateChar(gen)
|
|
||||||
charSet.delete(char)
|
|
||||||
}
|
|
||||||
t.log(`Charactes missing: ${charSet.size} - generating all of "${chars}"`)
|
|
||||||
t.assert(charSet.size === 0, 'Generated all documented characters.')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
|
|
||||||
/**
|
|
||||||
* A SimpleDiff describes a change on a String.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* console.log(a) // the old value
|
|
||||||
* console.log(b) // the updated value
|
|
||||||
* // Apply changes of diff (pseudocode)
|
|
||||||
* a.remove(diff.pos, diff.remove) // Remove `diff.remove` characters
|
|
||||||
* a.insert(diff.pos, diff.insert) // Insert `diff.insert`
|
|
||||||
* a === b // values match
|
|
||||||
*
|
|
||||||
* @typedef {Object} SimpleDiff
|
|
||||||
* @property {Number} pos The index where changes were applied
|
|
||||||
* @property {Number} remove The number of characters to delete starting
|
|
||||||
* at `index`.
|
|
||||||
* @property {String} insert The new text to insert at `index` after applying
|
|
||||||
* `delete`
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a diff between two strings. This diff implementation is highly
|
|
||||||
* efficient, but not very sophisticated.
|
|
||||||
*
|
|
||||||
* @public
|
|
||||||
* @param {String} a The old version of the string
|
|
||||||
* @param {String} b The updated version of the string
|
|
||||||
* @return {SimpleDiff} The diff description.
|
|
||||||
*/
|
|
||||||
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, // TODO: rename to index (also in type above)
|
|
||||||
remove: a.length - left - right,
|
|
||||||
insert: b.slice(left, b.length - right)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export const fromCharCode = String.fromCharCode
|
|
||||||
export const fromCodePoint = String.fromCodePoint
|
|
||||||
33
lib/test.js
33
lib/test.js
@@ -1,33 +0,0 @@
|
|||||||
import * as logging from './logging.js'
|
|
||||||
import simpleDiff from './simpleDiff.js'
|
|
||||||
|
|
||||||
export const run = async (name, f) => {
|
|
||||||
console.log(`%cStart:%c ${name}`, 'color:blue;', '')
|
|
||||||
const start = new Date()
|
|
||||||
try {
|
|
||||||
await f(name)
|
|
||||||
} catch (e) {
|
|
||||||
logging.print(`%cFailure:%c ${name} in %c${new Date().getTime() - start.getTime()}ms`, 'color:red;font-weight:bold', '', 'color:grey')
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
logging.print(`%cSuccess:%c ${name} in %c${new Date().getTime() - start.getTime()}ms`, 'color:green;font-weight:bold', '', 'color:grey')
|
|
||||||
}
|
|
||||||
|
|
||||||
export const compareArrays = (as, bs) => {
|
|
||||||
if (as.length !== bs.length) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for (let i = 0; i < as.length; i++) {
|
|
||||||
if (as[i] !== bs[i]) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
export const compareStrings = (a, b) => {
|
|
||||||
if (a !== b) {
|
|
||||||
const diff = simpleDiff(a, b)
|
|
||||||
logging.print(`%c${a.slice(0, diff.pos)}%c${a.slice(diff.pos, diff.remove)}%c${diff.insert}%c${a.slice(diff.pos + diff.remove)}`, 'color:grey', 'color:red', 'color:green', 'color:grey')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
|
|
||||||
export const getDate = () => new Date()
|
|
||||||
export const getUnixTime = () => getDate().getTime()
|
|
||||||
5798
package-lock.json
generated
5798
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
44
package.json
44
package.json
@@ -1,25 +1,21 @@
|
|||||||
{
|
{
|
||||||
"name": "yjs",
|
"name": "yjs",
|
||||||
"version": "13.0.0-66",
|
"version": "13.0.0-29",
|
||||||
"description": "A framework for real-time p2p shared editing on any data",
|
"description": "A framework for real-time p2p shared editing on any data",
|
||||||
"main": "./y.node.js",
|
"main": "./y.node.js",
|
||||||
"browser": "./y.js",
|
"browser": "./y.js",
|
||||||
"module": "./src/index.js",
|
"module": "./src/y.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "npm run lint",
|
"test": "npm run lint",
|
||||||
"debug": "concurrently 'rollup -wc rollup.test.js' 'cutest-serve y.test.js -o'",
|
"debug": "concurrently 'rollup -wc rollup.test.js' 'cutest-serve y.test.js -o'",
|
||||||
"lint": "standard src/**/*.js test/**/*.js tests-lib/**/*.js",
|
"lint": "standard",
|
||||||
"docs": "esdoc",
|
|
||||||
"serve-docs": "npm run docs && serve ./docs/",
|
|
||||||
"dist": "rollup -c rollup.browser.js; rollup -c rollup.node.js",
|
"dist": "rollup -c rollup.browser.js; rollup -c rollup.node.js",
|
||||||
"watch": "concurrently 'rollup -wc rollup.browser.js' 'rollup -wc rollup.node.js'",
|
"watch": "concurrently 'rollup -wc rollup.browser.js' 'rollup -wc rollup.node.js'",
|
||||||
"postversion": "npm run dist"
|
"postversion": "npm run dist",
|
||||||
|
"postpublish": "tag-dist-files --overwrite-existing-tag"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"y.*",
|
"y.*"
|
||||||
"src/*",
|
|
||||||
".esdoc.json",
|
|
||||||
"docs/*"
|
|
||||||
],
|
],
|
||||||
"standard": {
|
"standard": {
|
||||||
"ignore": [
|
"ignore": [
|
||||||
@@ -48,29 +44,29 @@
|
|||||||
},
|
},
|
||||||
"homepage": "http://y-js.org",
|
"homepage": "http://y-js.org",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel-cli": "^6.26.0",
|
"babel-cli": "^6.24.1",
|
||||||
"babel-plugin-external-helpers": "^6.22.0",
|
"babel-plugin-external-helpers": "^6.22.0",
|
||||||
"babel-plugin-transform-regenerator": "^6.26.0",
|
"babel-plugin-transform-regenerator": "^6.24.1",
|
||||||
"babel-plugin-transform-runtime": "^6.23.0",
|
"babel-plugin-transform-runtime": "^6.23.0",
|
||||||
"babel-preset-latest": "^6.24.1",
|
"babel-preset-latest": "^6.24.1",
|
||||||
"concurrently": "^3.6.1",
|
"chance": "^1.0.9",
|
||||||
|
"concurrently": "^3.4.0",
|
||||||
"cutest": "^0.1.9",
|
"cutest": "^0.1.9",
|
||||||
"esdoc": "^1.1.0",
|
|
||||||
"esdoc-standard-plugin": "^1.0.0",
|
|
||||||
"quill": "^1.3.6",
|
|
||||||
"quill-cursors": "^1.0.3",
|
|
||||||
"rollup": "^0.58.2",
|
|
||||||
"rollup-plugin-babel": "^2.7.1",
|
"rollup-plugin-babel": "^2.7.1",
|
||||||
"rollup-plugin-commonjs": "^8.4.1",
|
"rollup-plugin-commonjs": "^8.0.2",
|
||||||
"rollup-plugin-inject": "^2.2.0",
|
"rollup-plugin-inject": "^2.0.0",
|
||||||
"rollup-plugin-multi-entry": "^2.0.2",
|
"rollup-plugin-multi-entry": "^2.0.1",
|
||||||
"rollup-plugin-node-resolve": "^3.4.0",
|
"rollup-plugin-node-resolve": "^3.0.0",
|
||||||
"rollup-plugin-uglify": "^1.0.2",
|
"rollup-plugin-uglify": "^1.0.2",
|
||||||
"rollup-regenerator-runtime": "^6.23.1",
|
"rollup-regenerator-runtime": "^6.23.1",
|
||||||
"rollup-watch": "^3.2.2",
|
"rollup-watch": "^3.2.2",
|
||||||
"standard": "^11.0.1"
|
"standard": "^10.0.2",
|
||||||
|
"tag-dist-files": "^0.1.6"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ws": "^6.1.0"
|
"debug": "^2.6.8",
|
||||||
|
"fast-diff": "^1.1.2",
|
||||||
|
"utf-8": "^1.0.0",
|
||||||
|
"utf8": "^2.1.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,85 +0,0 @@
|
|||||||
/* eslint-env browser */
|
|
||||||
|
|
||||||
import * as Y from '../../src/index.js'
|
|
||||||
export * from '../../src/index.js'
|
|
||||||
|
|
||||||
const reconnectTimeout = 100
|
|
||||||
|
|
||||||
const setupWS = (doc, url) => {
|
|
||||||
const websocket = new WebSocket(url)
|
|
||||||
websocket.binaryType = 'arraybuffer'
|
|
||||||
doc.ws = websocket
|
|
||||||
websocket.onmessage = event => {
|
|
||||||
const decoder = Y.createDecoder(event.data)
|
|
||||||
const encoder = Y.createEncoder()
|
|
||||||
doc.mux(() =>
|
|
||||||
Y.readMessage(decoder, encoder, doc)
|
|
||||||
)
|
|
||||||
if (Y.length(encoder) > 0) {
|
|
||||||
websocket.send(Y.toBuffer(encoder))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
websocket.onclose = () => {
|
|
||||||
doc.ws = null
|
|
||||||
doc.wsconnected = false
|
|
||||||
doc.emit('status', {
|
|
||||||
status: 'connected'
|
|
||||||
})
|
|
||||||
setTimeout(setupWS, reconnectTimeout, doc, url)
|
|
||||||
}
|
|
||||||
websocket.onopen = () => {
|
|
||||||
doc.wsconnected = true
|
|
||||||
doc.emit('status', {
|
|
||||||
status: 'disconnected'
|
|
||||||
})
|
|
||||||
// always send sync step 1 when connected
|
|
||||||
const encoder = Y.createEncoder()
|
|
||||||
Y.writeSyncStep1(encoder, doc)
|
|
||||||
websocket.send(Y.toBuffer(encoder))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const broadcastUpdate = (y, transaction) => {
|
|
||||||
if (y.wsconnected && transaction.encodedStructsLen > 0) {
|
|
||||||
y.mux(() => {
|
|
||||||
const encoder = Y.createEncoder()
|
|
||||||
Y.writeUpdate(encoder, transaction.encodedStructsLen, transaction.encodedStructs)
|
|
||||||
y.ws.send(Y.toBuffer(encoder))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class WebsocketsSharedDocument extends Y.Y {
|
|
||||||
constructor (url) {
|
|
||||||
super()
|
|
||||||
this.wsconnected = false
|
|
||||||
this.mux = Y.createMutex()
|
|
||||||
setupWS(this, url)
|
|
||||||
this.on('afterTransaction', broadcastUpdate)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class WebsocketProvider {
|
|
||||||
constructor (url) {
|
|
||||||
// ensure that url is always ends with /
|
|
||||||
while (url[url.length - 1] === '/') {
|
|
||||||
url = url.slice(0, url.length - 1)
|
|
||||||
}
|
|
||||||
this.url = url + '/'
|
|
||||||
/**
|
|
||||||
* @type {Map<string, WebsocketsSharedDocument>}
|
|
||||||
*/
|
|
||||||
this.docs = new Map()
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* @param {string} name
|
|
||||||
* @return {WebsocketsSharedDocument}
|
|
||||||
*/
|
|
||||||
get (name) {
|
|
||||||
let doc = this.docs.get(name)
|
|
||||||
if (doc === undefined) {
|
|
||||||
doc = new WebsocketsSharedDocument(this.url + name)
|
|
||||||
}
|
|
||||||
return doc
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
const Y = require('../../build/node/index.js')
|
|
||||||
const WebSocket = require('ws')
|
|
||||||
const wss = new WebSocket.Server({ port: 1234 })
|
|
||||||
const docs = new Map()
|
|
||||||
|
|
||||||
const afterTransaction = (doc, transaction) => {
|
|
||||||
if (transaction.encodedStructsLen > 0) {
|
|
||||||
const encoder = Y.createEncoder()
|
|
||||||
Y.writeUpdate(encoder, transaction.encodedStructsLen, transaction.encodedStructs)
|
|
||||||
const message = Y.toBuffer(encoder)
|
|
||||||
doc.conns.forEach(conn => conn.send(message))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class WSSharedDoc extends Y.Y {
|
|
||||||
constructor () {
|
|
||||||
super()
|
|
||||||
this.mux = Y.createMutex()
|
|
||||||
this.conns = new Set()
|
|
||||||
this.on('afterTransaction', afterTransaction)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const messageListener = (conn, doc, message) => {
|
|
||||||
const encoder = Y.createEncoder()
|
|
||||||
const decoder = Y.createDecoder(message)
|
|
||||||
Y.readMessage(decoder, encoder, doc)
|
|
||||||
if (Y.length(encoder) > 0) {
|
|
||||||
conn.send(Y.toBuffer(encoder))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const setupConnection = (conn, req) => {
|
|
||||||
conn.binaryType = 'arraybuffer'
|
|
||||||
// get doc, create if it does not exist yet
|
|
||||||
let doc = docs.get(req.url.slice(1))
|
|
||||||
if (doc === undefined) {
|
|
||||||
doc = new WSSharedDoc()
|
|
||||||
docs.set(req.url.slice(1), doc)
|
|
||||||
}
|
|
||||||
doc.conns.add(conn)
|
|
||||||
// listen and reply to events
|
|
||||||
conn.on('message', message => messageListener(conn, doc, message))
|
|
||||||
conn.on('close', () =>
|
|
||||||
doc.conns.delete(conn)
|
|
||||||
)
|
|
||||||
// send sync step 1
|
|
||||||
const encoder = Y.createEncoder()
|
|
||||||
Y.writeSyncStep1(encoder, doc)
|
|
||||||
conn.send(Y.toBuffer(encoder))
|
|
||||||
}
|
|
||||||
|
|
||||||
wss.on('connection', setupConnection)
|
|
||||||
@@ -5,13 +5,9 @@ import commonjs from 'rollup-plugin-commonjs'
|
|||||||
var pkg = require('./package.json')
|
var pkg = require('./package.json')
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
input: 'src/Y.dist.js',
|
entry: 'src/Y.js',
|
||||||
name: 'Y',
|
moduleName: 'Y',
|
||||||
sourcemap: true,
|
format: 'umd',
|
||||||
output: {
|
|
||||||
file: 'y.js',
|
|
||||||
format: 'umd'
|
|
||||||
},
|
|
||||||
plugins: [
|
plugins: [
|
||||||
nodeResolve({
|
nodeResolve({
|
||||||
main: true,
|
main: true,
|
||||||
@@ -22,7 +18,7 @@ export default {
|
|||||||
babel(),
|
babel(),
|
||||||
uglify({
|
uglify({
|
||||||
mangle: {
|
mangle: {
|
||||||
except: ['YMap', 'Y', 'YArray', 'YText', 'YXmlHook', 'YXmlFragment', 'YXmlElement', 'YXmlEvent', 'YXmlText', 'YEvent', 'YArrayEvent', 'YMapEvent', 'Type', 'Delete', 'ItemJSON', 'ItemString', 'Item']
|
except: ['YMap', 'Y', 'YArray', 'YText', 'YXmlFragment', 'YXmlElement', 'YXmlEvent', 'YXmlText', 'YEvent', 'YArrayEvent', 'YMapEvent', 'Type', 'Delete', 'ItemJSON', 'ItemString', 'Item']
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
comments: function (node, comment) {
|
comments: function (node, comment) {
|
||||||
@@ -36,6 +32,8 @@ export default {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
|
dest: 'y.js',
|
||||||
|
sourceMap: true,
|
||||||
banner: `
|
banner: `
|
||||||
/**
|
/**
|
||||||
* ${pkg.name} - ${pkg.description}
|
* ${pkg.name} - ${pkg.description}
|
||||||
|
|||||||
@@ -1,18 +1,26 @@
|
|||||||
const pkg = require('./package.json')
|
import nodeResolve from 'rollup-plugin-node-resolve'
|
||||||
|
import commonjs from 'rollup-plugin-commonjs'
|
||||||
|
var pkg = require('./package.json')
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
input: 'src/index.js',
|
entry: 'src/y-dist.cjs.js',
|
||||||
output: {
|
moduleName: 'Y',
|
||||||
name: 'Y',
|
format: 'cjs',
|
||||||
file: 'build/node/index.js',
|
plugins: [
|
||||||
format: 'cjs',
|
nodeResolve({
|
||||||
sourcemap: true,
|
main: true,
|
||||||
banner: `
|
module: true,
|
||||||
|
browser: true
|
||||||
|
}),
|
||||||
|
commonjs()
|
||||||
|
],
|
||||||
|
dest: 'y.node.js',
|
||||||
|
sourceMap: true,
|
||||||
|
banner: `
|
||||||
/**
|
/**
|
||||||
* ${pkg.name} - ${pkg.description}
|
* ${pkg.name} - ${pkg.description}
|
||||||
* @version v${pkg.version}
|
* @version v${pkg.version}
|
||||||
* @license ${pkg.license}
|
* @license ${pkg.license}
|
||||||
*/
|
*/
|
||||||
`
|
`
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,20 +3,18 @@ import commonjs from 'rollup-plugin-commonjs'
|
|||||||
import multiEntry from 'rollup-plugin-multi-entry'
|
import multiEntry from 'rollup-plugin-multi-entry'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
input: 'test/index.js',
|
entry: 'test/y-xml.tests.js',
|
||||||
name: 'y-tests',
|
moduleName: 'y-tests',
|
||||||
sourcemap: true,
|
format: 'umd',
|
||||||
output: {
|
|
||||||
file: 'y.test.js',
|
|
||||||
format: 'umd'
|
|
||||||
},
|
|
||||||
plugins: [
|
plugins: [
|
||||||
multiEntry(),
|
|
||||||
nodeResolve({
|
nodeResolve({
|
||||||
main: true,
|
main: true,
|
||||||
module: true,
|
module: true,
|
||||||
browser: true
|
browser: true
|
||||||
}),
|
}),
|
||||||
commonjs()
|
commonjs(),
|
||||||
]
|
multiEntry()
|
||||||
|
],
|
||||||
|
dest: 'y.test.js',
|
||||||
|
sourceMap: true
|
||||||
}
|
}
|
||||||
|
|||||||
120
src/Binary/Decoder.js
Normal file
120
src/Binary/Decoder.js
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import utf8 from 'utf-8'
|
||||||
|
import ID from '../Util/ID.js'
|
||||||
|
import { default as RootID, RootFakeUserID } from '../Util/RootID.js'
|
||||||
|
|
||||||
|
export default class BinaryDecoder {
|
||||||
|
constructor (buffer) {
|
||||||
|
if (buffer instanceof ArrayBuffer) {
|
||||||
|
this.uint8arr = new Uint8Array(buffer)
|
||||||
|
} else if (buffer instanceof Uint8Array || (typeof Buffer !== 'undefined' && buffer instanceof Buffer)) {
|
||||||
|
this.uint8arr = buffer
|
||||||
|
} else {
|
||||||
|
throw new Error('Expected an ArrayBuffer or Uint8Array!')
|
||||||
|
}
|
||||||
|
this.pos = 0
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Clone this decoder instance
|
||||||
|
* Optionally set a new position parameter
|
||||||
|
*/
|
||||||
|
clone (newPos = this.pos) {
|
||||||
|
let decoder = new BinaryDecoder(this.uint8arr)
|
||||||
|
decoder.pos = newPos
|
||||||
|
return decoder
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Number of bytes
|
||||||
|
*/
|
||||||
|
get length () {
|
||||||
|
return this.uint8arr.length
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Skip one byte, jump to the next position
|
||||||
|
*/
|
||||||
|
skip8 () {
|
||||||
|
this.pos++
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Read one byte as unsigned integer
|
||||||
|
*/
|
||||||
|
readUint8 () {
|
||||||
|
return this.uint8arr[this.pos++]
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Read 4 bytes as unsigned integer
|
||||||
|
*/
|
||||||
|
readUint32 () {
|
||||||
|
let uint =
|
||||||
|
this.uint8arr[this.pos] +
|
||||||
|
(this.uint8arr[this.pos + 1] << 8) +
|
||||||
|
(this.uint8arr[this.pos + 2] << 16) +
|
||||||
|
(this.uint8arr[this.pos + 3] << 24)
|
||||||
|
this.pos += 4
|
||||||
|
return uint
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Look ahead without incrementing position
|
||||||
|
* to the next byte and read it as unsigned integer
|
||||||
|
*/
|
||||||
|
peekUint8 () {
|
||||||
|
return this.uint8arr[this.pos]
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Read unsigned integer (32bit) with variable length
|
||||||
|
* 1/8th of the storage is used as encoding overhead
|
||||||
|
* - numbers < 2^7 is stored in one byte
|
||||||
|
* - numbers < 2^14 is stored in two bytes
|
||||||
|
* ..
|
||||||
|
*/
|
||||||
|
readVarUint () {
|
||||||
|
let num = 0
|
||||||
|
let len = 0
|
||||||
|
while (true) {
|
||||||
|
let r = this.uint8arr[this.pos++]
|
||||||
|
num = num | ((r & 0b1111111) << len)
|
||||||
|
len += 7
|
||||||
|
if (r < 1 << 7) {
|
||||||
|
return num >>> 0 // return unsigned number!
|
||||||
|
}
|
||||||
|
if (len > 35) {
|
||||||
|
throw new Error('Integer out of range!')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Read string of variable length
|
||||||
|
* - varUint is used to store the length of the string
|
||||||
|
*/
|
||||||
|
readVarString () {
|
||||||
|
let len = this.readVarUint()
|
||||||
|
let bytes = new Array(len)
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
bytes[i] = this.uint8arr[this.pos++]
|
||||||
|
}
|
||||||
|
return utf8.getStringFromBytes(bytes)
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Look ahead and read varString without incrementing position
|
||||||
|
*/
|
||||||
|
peekVarString () {
|
||||||
|
let pos = this.pos
|
||||||
|
let s = this.readVarString()
|
||||||
|
this.pos = pos
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Read ID
|
||||||
|
* - If first varUint read is 0xFFFFFF a RootID is returned
|
||||||
|
* - Otherwise an ID is returned
|
||||||
|
*/
|
||||||
|
readID () {
|
||||||
|
let user = this.readVarUint()
|
||||||
|
if (user === RootFakeUserID) {
|
||||||
|
// read property name and type id
|
||||||
|
const rid = new RootID(this.readVarString(), null)
|
||||||
|
rid.type = this.readVarUint()
|
||||||
|
return rid
|
||||||
|
}
|
||||||
|
return new ID(user, this.readVarUint())
|
||||||
|
}
|
||||||
|
}
|
||||||
83
src/Binary/Encoder.js
Normal file
83
src/Binary/Encoder.js
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import utf8 from 'utf-8'
|
||||||
|
import { RootFakeUserID } from '../Util/RootID.js'
|
||||||
|
|
||||||
|
const bits7 = 0b1111111
|
||||||
|
const bits8 = 0b11111111
|
||||||
|
|
||||||
|
export default class BinaryEncoder {
|
||||||
|
constructor () {
|
||||||
|
// TODO: implement chained Uint8Array buffers instead of Array buffer
|
||||||
|
this.data = []
|
||||||
|
}
|
||||||
|
|
||||||
|
get length () {
|
||||||
|
return this.data.length
|
||||||
|
}
|
||||||
|
|
||||||
|
get pos () {
|
||||||
|
return this.data.length
|
||||||
|
}
|
||||||
|
|
||||||
|
createBuffer () {
|
||||||
|
return Uint8Array.from(this.data).buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
writeUint8 (num) {
|
||||||
|
this.data.push(num & bits8)
|
||||||
|
}
|
||||||
|
|
||||||
|
setUint8 (pos, num) {
|
||||||
|
this.data[pos] = num & bits8
|
||||||
|
}
|
||||||
|
|
||||||
|
writeUint16 (num) {
|
||||||
|
this.data.push(num & bits8, (num >>> 8) & bits8)
|
||||||
|
}
|
||||||
|
|
||||||
|
setUint16 (pos, num) {
|
||||||
|
this.data[pos] = num & bits8
|
||||||
|
this.data[pos + 1] = (num >>> 8) & bits8
|
||||||
|
}
|
||||||
|
|
||||||
|
writeUint32 (num) {
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
this.data.push(num & bits8)
|
||||||
|
num >>>= 8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setUint32 (pos, num) {
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
this.data[pos + i] = num & bits8
|
||||||
|
num >>>= 8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeVarUint (num) {
|
||||||
|
while (num >= 0b10000000) {
|
||||||
|
this.data.push(0b10000000 | (bits7 & num))
|
||||||
|
num >>>= 7
|
||||||
|
}
|
||||||
|
this.data.push(bits7 & num)
|
||||||
|
}
|
||||||
|
|
||||||
|
writeVarString (str) {
|
||||||
|
let bytes = utf8.setBytesFromString(str)
|
||||||
|
let len = bytes.length
|
||||||
|
this.writeVarUint(len)
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
this.data.push(bytes[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeID (id) {
|
||||||
|
const user = id.user
|
||||||
|
this.writeVarUint(user)
|
||||||
|
if (user !== RootFakeUserID) {
|
||||||
|
this.writeVarUint(id.clock)
|
||||||
|
} else {
|
||||||
|
this.writeVarString(id.name)
|
||||||
|
this.writeVarUint(id.type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
|
|
||||||
import { createMutex } from '../../lib/mutex.js'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Abstract class for bindings.
|
|
||||||
*
|
|
||||||
* A binding handles data binding from a Yjs type to a data object. For example,
|
|
||||||
* you can bind a Quill editor instance to a YText instance with the `QuillBinding` class.
|
|
||||||
*
|
|
||||||
* It is expected that a concrete implementation accepts two parameters
|
|
||||||
* (type and binding target).
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* const quill = new Quill(document.createElement('div'))
|
|
||||||
* const type = y.define('quill', Y.Text)
|
|
||||||
* const binding = new Y.QuillBinding(quill, type)
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export default class Binding {
|
|
||||||
/**
|
|
||||||
* @param {YType} type Yjs type.
|
|
||||||
* @param {any} target Binding Target.
|
|
||||||
*/
|
|
||||||
constructor (type, target) {
|
|
||||||
/**
|
|
||||||
* The Yjs type that is bound to `target`
|
|
||||||
* @type {YType}
|
|
||||||
*/
|
|
||||||
this.type = type
|
|
||||||
/**
|
|
||||||
* The target that `type` is bound to.
|
|
||||||
* @type {*}
|
|
||||||
*/
|
|
||||||
this.target = target
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
this._mutualExclude = createMutex()
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Remove all data observers (both from the type and the target).
|
|
||||||
*/
|
|
||||||
destroy () {
|
|
||||||
this.type = null
|
|
||||||
this.target = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
|
|
||||||
import Binding from '../Binding.js'
|
|
||||||
import simpleDiff from '../../Util/simpleDiff.js'
|
|
||||||
import { getRelativePosition, fromRelativePosition } from '../../Util/relativePosition.js'
|
|
||||||
|
|
||||||
function typeObserver () {
|
|
||||||
this._mutualExclude(() => {
|
|
||||||
const textarea = this.target
|
|
||||||
const textType = this.type
|
|
||||||
const relativeStart = getRelativePosition(textType, textarea.selectionStart)
|
|
||||||
const relativeEnd = getRelativePosition(textType, textarea.selectionEnd)
|
|
||||||
textarea.value = textType.toString()
|
|
||||||
const start = fromRelativePosition(textType._y, relativeStart)
|
|
||||||
const end = fromRelativePosition(textType._y, relativeEnd)
|
|
||||||
textarea.setSelectionRange(start, end)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function domObserver () {
|
|
||||||
this._mutualExclude(() => {
|
|
||||||
let diff = simpleDiff(this.type.toString(), this.target.value)
|
|
||||||
this.type.delete(diff.pos, diff.remove)
|
|
||||||
this.type.insert(diff.pos, diff.insert)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A binding that binds a YText to a dom textarea.
|
|
||||||
*
|
|
||||||
* This binding is automatically destroyed when its parent is deleted.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* const textare = document.createElement('textarea')
|
|
||||||
* const type = y.define('textarea', Y.Text)
|
|
||||||
* const binding = new Y.QuillBinding(type, textarea)
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export default class TextareaBinding extends Binding {
|
|
||||||
constructor (textType, domTextarea) {
|
|
||||||
// Binding handles textType as this.type and domTextarea as this.target
|
|
||||||
super(textType, domTextarea)
|
|
||||||
// set initial value
|
|
||||||
domTextarea.value = textType.toString()
|
|
||||||
// Observers are handled by this class
|
|
||||||
this._typeObserver = typeObserver.bind(this)
|
|
||||||
this._domObserver = domObserver.bind(this)
|
|
||||||
textType.observe(this._typeObserver)
|
|
||||||
domTextarea.addEventListener('input', this._domObserver)
|
|
||||||
}
|
|
||||||
destroy () {
|
|
||||||
// Remove everything that is handled by this class
|
|
||||||
this.type.unobserve(this._typeObserver)
|
|
||||||
this.target.unobserve(this._domObserver)
|
|
||||||
super.destroy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,211 +0,0 @@
|
|||||||
/* global MutationObserver, getSelection */
|
|
||||||
|
|
||||||
import { fromRelativePosition } from '../../Util/relativePosition.js'
|
|
||||||
import Binding from '../Binding.js'
|
|
||||||
import { createAssociation, removeAssociation } from './util.js'
|
|
||||||
import { beforeTransactionSelectionFixer, afterTransactionSelectionFixer, getCurrentRelativeSelection } from './selection.js'
|
|
||||||
import { defaultFilter, applyFilterOnType } from './filter.js'
|
|
||||||
import typeObserver from './typeObserver.js'
|
|
||||||
import domObserver from './domObserver.js'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {import('./filter.js').DomFilter} DomFilter
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A binding that binds the children of a YXmlFragment to a DOM element.
|
|
||||||
*
|
|
||||||
* This binding is automatically destroyed when its parent is deleted.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* const div = document.createElement('div')
|
|
||||||
* const type = y.define('xml', Y.XmlFragment)
|
|
||||||
* const binding = new Y.QuillBinding(type, div)
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export default class DomBinding extends Binding {
|
|
||||||
/**
|
|
||||||
* @param {YXmlFragment} type The bind source. This is the ultimate source of
|
|
||||||
* truth.
|
|
||||||
* @param {Element} target The bind target. Mirrors the target.
|
|
||||||
* @param {Object} [opts] Optional configurations
|
|
||||||
|
|
||||||
* @param {DomFilter} [opts.filter=defaultFilter] The filter function to use.
|
|
||||||
*/
|
|
||||||
constructor (type, target, opts = {}) {
|
|
||||||
// Binding handles textType as this.type and domTextarea as this.target
|
|
||||||
super(type, target)
|
|
||||||
this.opts = opts
|
|
||||||
opts.document = opts.document || document
|
|
||||||
opts.hooks = opts.hooks || {}
|
|
||||||
this.scrollingElement = opts.scrollingElement || null
|
|
||||||
/**
|
|
||||||
* Maps each DOM element to the type that it is associated with.
|
|
||||||
* @type {Map}
|
|
||||||
*/
|
|
||||||
this.domToType = new Map()
|
|
||||||
/**
|
|
||||||
* Maps each YXml type to the DOM element that it is associated with.
|
|
||||||
* @type {Map}
|
|
||||||
*/
|
|
||||||
this.typeToDom = new Map()
|
|
||||||
/**
|
|
||||||
* Defines which DOM attributes and elements to filter out.
|
|
||||||
* Also filters remote changes.
|
|
||||||
* @type {DomFilter}
|
|
||||||
*/
|
|
||||||
this.filter = opts.filter || defaultFilter
|
|
||||||
// set initial value
|
|
||||||
target.innerHTML = ''
|
|
||||||
type.forEach(child => {
|
|
||||||
target.insertBefore(child.toDom(opts.document, opts.hooks, this), null)
|
|
||||||
})
|
|
||||||
this._typeObserver = typeObserver.bind(this)
|
|
||||||
this._domObserver = mutations => {
|
|
||||||
domObserver.call(this, mutations, opts.document)
|
|
||||||
}
|
|
||||||
type.observeDeep(this._typeObserver)
|
|
||||||
this._mutationObserver = new MutationObserver(this._domObserver)
|
|
||||||
this._mutationObserver.observe(target, {
|
|
||||||
childList: true,
|
|
||||||
attributes: true,
|
|
||||||
characterData: true,
|
|
||||||
subtree: true
|
|
||||||
})
|
|
||||||
this._currentSel = null
|
|
||||||
this._selectionchange = () => {
|
|
||||||
this._currentSel = getCurrentRelativeSelection(this)
|
|
||||||
}
|
|
||||||
document.addEventListener('selectionchange', this._selectionchange)
|
|
||||||
const y = type._y
|
|
||||||
this.y = y
|
|
||||||
// Force flush dom changes before Type changes are applied (they might
|
|
||||||
// modify the dom)
|
|
||||||
this._beforeTransactionHandler = (y, transaction, remote) => {
|
|
||||||
this._domObserver(this._mutationObserver.takeRecords())
|
|
||||||
this._mutualExclude(() => {
|
|
||||||
beforeTransactionSelectionFixer(this, remote)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
y.on('beforeTransaction', this._beforeTransactionHandler)
|
|
||||||
this._afterTransactionHandler = (y, transaction, remote) => {
|
|
||||||
this._mutualExclude(() => {
|
|
||||||
afterTransactionSelectionFixer(this, remote)
|
|
||||||
})
|
|
||||||
// remove associations
|
|
||||||
// TODO: this could be done more efficiently
|
|
||||||
// e.g. Always delete using the following approach, or removeAssociation
|
|
||||||
// in dom/type-observer..
|
|
||||||
transaction.deletedStructs.forEach(type => {
|
|
||||||
const dom = this.typeToDom.get(type)
|
|
||||||
if (dom !== undefined) {
|
|
||||||
removeAssociation(this, dom, type)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
y.on('afterTransaction', this._afterTransactionHandler)
|
|
||||||
// Before calling observers, apply dom filter to all changed and new types.
|
|
||||||
this._beforeObserverCallsHandler = (y, transaction) => {
|
|
||||||
// Apply dom filter to new and changed types
|
|
||||||
transaction.changedTypes.forEach((subs, type) => {
|
|
||||||
// Only check attributes. New types are filtered below.
|
|
||||||
if ((subs.size > 1 || (subs.size === 1 && subs.has(null) === false))) {
|
|
||||||
applyFilterOnType(y, this, type)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
transaction.newTypes.forEach(type => {
|
|
||||||
applyFilterOnType(y, this, type)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
y.on('beforeObserverCalls', this._beforeObserverCallsHandler)
|
|
||||||
createAssociation(this, target, type)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* NOTE: currently does not apply filter to existing elements!
|
|
||||||
* @param {DomFilter} filter The filter function to use from now on.
|
|
||||||
*/
|
|
||||||
setFilter (filter) {
|
|
||||||
this.filter = filter
|
|
||||||
// TODO: apply filter to all elements
|
|
||||||
}
|
|
||||||
|
|
||||||
_getUndoStackInfo () {
|
|
||||||
return this.getSelection()
|
|
||||||
}
|
|
||||||
|
|
||||||
_restoreUndoStackInfo (info) {
|
|
||||||
this.restoreSelection(info)
|
|
||||||
}
|
|
||||||
|
|
||||||
getSelection () {
|
|
||||||
return this._currentSel
|
|
||||||
}
|
|
||||||
|
|
||||||
restoreSelection (selection) {
|
|
||||||
if (selection !== null) {
|
|
||||||
const { to, from } = selection
|
|
||||||
/**
|
|
||||||
* There is little information on the difference between anchor/focus and base/extent.
|
|
||||||
* MDN doesn't even mention base/extent anymore.. though you still have to call
|
|
||||||
* setBaseAndExtent to change the selection..
|
|
||||||
* I can observe that base/extend refer to notes higher up in the xml hierachy.
|
|
||||||
* Espesially for undo/redo this is preferred. If this becomes a problem in the future,
|
|
||||||
* we should probably go back to anchor/focus.
|
|
||||||
*/
|
|
||||||
const browserSelection = getSelection()
|
|
||||||
let { baseNode, baseOffset, extentNode, extentOffset } = browserSelection
|
|
||||||
if (from !== null) {
|
|
||||||
let sel = fromRelativePosition(this.y, from)
|
|
||||||
if (sel !== null) {
|
|
||||||
let node = this.typeToDom.get(sel.type)
|
|
||||||
let offset = sel.offset
|
|
||||||
if (node !== baseNode || offset !== baseOffset) {
|
|
||||||
baseNode = node
|
|
||||||
baseOffset = offset
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (to !== null) {
|
|
||||||
let sel = fromRelativePosition(this.y, to)
|
|
||||||
if (sel !== null) {
|
|
||||||
let node = this.typeToDom.get(sel.type)
|
|
||||||
let offset = sel.offset
|
|
||||||
if (node !== extentNode || offset !== extentOffset) {
|
|
||||||
extentNode = node
|
|
||||||
extentOffset = offset
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
browserSelection.setBaseAndExtent(
|
|
||||||
baseNode,
|
|
||||||
baseOffset,
|
|
||||||
extentNode,
|
|
||||||
extentOffset
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove all properties that are handled by this class.
|
|
||||||
*/
|
|
||||||
destroy () {
|
|
||||||
this.domToType = null
|
|
||||||
this.typeToDom = null
|
|
||||||
this.type.unobserveDeep(this._typeObserver)
|
|
||||||
this._mutationObserver.disconnect()
|
|
||||||
const y = this.type._y
|
|
||||||
y.off('beforeTransaction', this._beforeTransactionHandler)
|
|
||||||
y.off('beforeObserverCalls', this._beforeObserverCallsHandler)
|
|
||||||
y.off('afterTransaction', this._afterTransactionHandler)
|
|
||||||
document.removeEventListener('selectionchange', this._selectionchange)
|
|
||||||
super.destroy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* A filter defines which elements and attributes to share.
|
|
||||||
* Return null if the node should be filtered. Otherwise return the Map of
|
|
||||||
* accepted attributes.
|
|
||||||
*
|
|
||||||
* @typedef {function(nodeName: String, attrs: Map): Map|null} FilterFunction
|
|
||||||
*/
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
|
|
||||||
import YXmlHook from '../../Types/YXml/YXmlHook.js'
|
|
||||||
import {
|
|
||||||
iterateUntilUndeleted,
|
|
||||||
removeAssociation,
|
|
||||||
insertNodeHelper } from './util.js'
|
|
||||||
import diff from '../../../lib/simpleDiff.js'
|
|
||||||
import YXmlFragment from '../../Types/YXml/YXmlFragment.js'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 1. Check if any of the nodes was deleted
|
|
||||||
* 2. Iterate over the children.
|
|
||||||
* 2.1 If a node exists that is not yet bound to a type, insert a new node
|
|
||||||
* 2.2 If _contents.length < dom.childNodes.length, fill the
|
|
||||||
* rest of _content with childNodes
|
|
||||||
* 2.3 If a node was moved, delete it and
|
|
||||||
* recreate a new yxml element that is bound to that node.
|
|
||||||
* You can detect that a node was moved because expectedId
|
|
||||||
* !== actualId in the list
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
function applyChangesFromDom (binding, dom, yxml, _document) {
|
|
||||||
if (yxml == null || yxml === false || yxml.constructor === YXmlHook) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const y = yxml._y
|
|
||||||
const knownChildren = new Set()
|
|
||||||
for (let i = dom.childNodes.length - 1; i >= 0; i--) {
|
|
||||||
const type = binding.domToType.get(dom.childNodes[i])
|
|
||||||
if (type !== undefined && type !== false) {
|
|
||||||
knownChildren.add(type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 1. Check if any of the nodes was deleted
|
|
||||||
yxml.forEach(function (childType) {
|
|
||||||
if (knownChildren.has(childType) === false) {
|
|
||||||
childType._delete(y)
|
|
||||||
removeAssociation(binding, binding.typeToDom.get(childType), childType)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// 2. iterate
|
|
||||||
const childNodes = dom.childNodes
|
|
||||||
const len = childNodes.length
|
|
||||||
let prevExpectedType = null
|
|
||||||
let expectedType = iterateUntilUndeleted(yxml._start)
|
|
||||||
for (let domCnt = 0; domCnt < len; domCnt++) {
|
|
||||||
const childNode = childNodes[domCnt]
|
|
||||||
const childType = binding.domToType.get(childNode)
|
|
||||||
if (childType !== undefined) {
|
|
||||||
if (childType === false) {
|
|
||||||
// should be ignored or is going to be deleted
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (expectedType !== null) {
|
|
||||||
if (expectedType !== childType) {
|
|
||||||
// 2.3 Not expected node
|
|
||||||
if (childType._parent !== yxml) {
|
|
||||||
// child was moved from another parent
|
|
||||||
// childType is going to be deleted by its previous parent
|
|
||||||
removeAssociation(binding, childNode, childType)
|
|
||||||
} else {
|
|
||||||
// child was moved to a different position.
|
|
||||||
removeAssociation(binding, childNode, childType)
|
|
||||||
childType._delete(y)
|
|
||||||
}
|
|
||||||
prevExpectedType = insertNodeHelper(yxml, prevExpectedType, childNode, _document, binding)
|
|
||||||
} else {
|
|
||||||
// Found expected node. Continue.
|
|
||||||
prevExpectedType = expectedType
|
|
||||||
expectedType = iterateUntilUndeleted(expectedType._right)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 2.2 Fill _content with child nodes
|
|
||||||
prevExpectedType = insertNodeHelper(yxml, prevExpectedType, childNode, _document, binding)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 2.1 A new node was found
|
|
||||||
prevExpectedType = insertNodeHelper(yxml, prevExpectedType, childNode, _document, binding)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
export default function domObserver (mutations, _document) {
|
|
||||||
this._mutualExclude(() => {
|
|
||||||
this.type._y.transact(() => {
|
|
||||||
let diffChildren = new Set()
|
|
||||||
mutations.forEach(mutation => {
|
|
||||||
const dom = mutation.target
|
|
||||||
const yxml = this.domToType.get(dom)
|
|
||||||
if (yxml === undefined) { // In case yxml is undefined, we double check if we forgot to bind the dom
|
|
||||||
let parent = dom
|
|
||||||
let yParent
|
|
||||||
do {
|
|
||||||
parent = parent.parentElement
|
|
||||||
yParent = this.domToType.get(parent)
|
|
||||||
} while (yParent === undefined && parent !== null)
|
|
||||||
if (yParent !== false && yParent !== undefined && yParent.constructor !== YXmlHook) {
|
|
||||||
diffChildren.add(parent)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
} else if (yxml === false || yxml.constructor === YXmlHook) {
|
|
||||||
// dom element is filtered / a dom hook
|
|
||||||
return
|
|
||||||
}
|
|
||||||
switch (mutation.type) {
|
|
||||||
case 'characterData':
|
|
||||||
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) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
let name = mutation.attributeName
|
|
||||||
let val = dom.getAttribute(name)
|
|
||||||
// check if filter accepts attribute
|
|
||||||
let attributes = new Map()
|
|
||||||
attributes.set(name, val)
|
|
||||||
if (yxml.constructor !== YXmlFragment && this.filter(dom.nodeName, attributes).size > 0) {
|
|
||||||
if (yxml.getAttribute(name) !== val) {
|
|
||||||
if (val == null) {
|
|
||||||
yxml.removeAttribute(name)
|
|
||||||
} else {
|
|
||||||
yxml.setAttribute(name, val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case 'childList':
|
|
||||||
diffChildren.add(mutation.target)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
})
|
|
||||||
for (let dom of diffChildren) {
|
|
||||||
const yxml = this.domToType.get(dom)
|
|
||||||
applyChangesFromDom(this, dom, yxml, _document)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
/* eslint-env browser */
|
|
||||||
import YXmlText from '../../Types/YXml/YXmlText.js'
|
|
||||||
import YXmlHook from '../../Types/YXml/YXmlHook.js'
|
|
||||||
import YXmlElement from '../../Types/YXml/YXmlElement.js'
|
|
||||||
import { createAssociation, domsToTypes } from './util.js'
|
|
||||||
import { filterDomAttributes, defaultFilter } from './filter.js'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {import('./filter.js').DomFilter} DomFilter
|
|
||||||
* @typedef {import('./DomBinding.js').default} DomBinding
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a Yjs type (YXml) based on the contents of a DOM Element.
|
|
||||||
*
|
|
||||||
* @param {Element|Text} element The DOM Element
|
|
||||||
* @param {?Document} _document Optional. Provide the global document object
|
|
||||||
* @param {Object<string, any>} [hooks = {}] Optional. Set of Yjs Hooks
|
|
||||||
* @param {DomFilter} [filter=defaultFilter] Optional. Dom element filter
|
|
||||||
* @param {?DomBinding} binding Warning: This property is for internal use only!
|
|
||||||
* @return {YXmlElement | YXmlText | false}
|
|
||||||
*/
|
|
||||||
export default function domToType (element, _document = document, hooks = {}, filter = defaultFilter, binding) {
|
|
||||||
/**
|
|
||||||
* @type {any}
|
|
||||||
*/
|
|
||||||
let type = null
|
|
||||||
if (element instanceof Element) {
|
|
||||||
let hookName = null
|
|
||||||
let hook
|
|
||||||
// configure `hookName !== undefined` if element is a hook.
|
|
||||||
if (element.hasAttribute('data-yjs-hook')) {
|
|
||||||
hookName = element.getAttribute('data-yjs-hook')
|
|
||||||
hook = hooks[hookName]
|
|
||||||
if (hook === undefined) {
|
|
||||||
console.error(`Unknown hook "${hookName}". Deleting yjsHook dataset property.`)
|
|
||||||
element.removeAttribute('data-yjs-hook')
|
|
||||||
hookName = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (hookName === null) {
|
|
||||||
// Not a hook
|
|
||||||
const attrs = filterDomAttributes(element, filter)
|
|
||||||
if (attrs === null) {
|
|
||||||
type = false
|
|
||||||
} else {
|
|
||||||
type = new YXmlElement(element.nodeName)
|
|
||||||
attrs.forEach((val, key) => {
|
|
||||||
type.setAttribute(key, val)
|
|
||||||
})
|
|
||||||
type.insert(0, domsToTypes(element.childNodes, document, hooks, filter, binding))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Is a hook
|
|
||||||
type = new YXmlHook(hookName)
|
|
||||||
hook.fillType(element, type)
|
|
||||||
}
|
|
||||||
} else if (element instanceof Text) {
|
|
||||||
type = new YXmlText()
|
|
||||||
type.insert(0, element.nodeValue)
|
|
||||||
} else {
|
|
||||||
throw new Error('Can\'t transform this node type to a YXml type!')
|
|
||||||
}
|
|
||||||
createAssociation(binding, element, type)
|
|
||||||
return type
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import isParentOf from '../../Util/isParentOf.js'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @callback DomFilter
|
|
||||||
* @param {string} nodeName
|
|
||||||
* @param {Map<string, string>} attrs
|
|
||||||
* @return {Map | null}
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default filter method (does nothing).
|
|
||||||
*
|
|
||||||
* @param {String} nodeName The nodeName of the element
|
|
||||||
* @param {Map} attrs Map of key-value pairs that are attributes of the node.
|
|
||||||
* @return {Map | null} The allowed attributes or null, if the element should be
|
|
||||||
* filtered.
|
|
||||||
*/
|
|
||||||
export function defaultFilter (nodeName, attrs) {
|
|
||||||
// TODO: implement basic filter that filters out dangerous properties!
|
|
||||||
return attrs
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export function filterDomAttributes (dom, filter) {
|
|
||||||
const attrs = new Map()
|
|
||||||
for (let i = dom.attributes.length - 1; i >= 0; i--) {
|
|
||||||
const attr = dom.attributes[i]
|
|
||||||
attrs.set(attr.name, attr.value)
|
|
||||||
}
|
|
||||||
return filter(dom.nodeName, attrs)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Applies a filter on a type.
|
|
||||||
*
|
|
||||||
* @param {Y} y The Yjs instance.
|
|
||||||
* @param {DomBinding} binding The DOM binding instance that has the dom filter.
|
|
||||||
* @param {YXmlElement | YXmlFragment } type The type to apply the filter to.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
export function applyFilterOnType (y, binding, type) {
|
|
||||||
if (isParentOf(binding.type, type)) {
|
|
||||||
const nodeName = type.nodeName
|
|
||||||
let attributes = new Map()
|
|
||||||
if (type.getAttributes !== undefined) {
|
|
||||||
let attrs = type.getAttributes()
|
|
||||||
for (let key in attrs) {
|
|
||||||
attributes.set(key, attrs[key])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const filteredAttributes = binding.filter(nodeName, new Map(attributes))
|
|
||||||
if (filteredAttributes === null) {
|
|
||||||
type._delete(y)
|
|
||||||
} else {
|
|
||||||
// iterate original attributes
|
|
||||||
attributes.forEach((value, key) => {
|
|
||||||
// delete all attributes that are not in filteredAttributes
|
|
||||||
if (filteredAttributes.has(key) === false) {
|
|
||||||
type.removeAttribute(key)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
/* globals getSelection */
|
|
||||||
|
|
||||||
import { getRelativePosition } from '../../Util/relativePosition.js'
|
|
||||||
|
|
||||||
let relativeSelection = null
|
|
||||||
|
|
||||||
function _getCurrentRelativeSelection (domBinding) {
|
|
||||||
const { baseNode, baseOffset, extentNode, extentOffset } = getSelection()
|
|
||||||
const baseNodeType = domBinding.domToType.get(baseNode)
|
|
||||||
const extentNodeType = domBinding.domToType.get(extentNode)
|
|
||||||
if (baseNodeType !== undefined && extentNodeType !== undefined) {
|
|
||||||
return {
|
|
||||||
from: getRelativePosition(baseNodeType, baseOffset),
|
|
||||||
to: getRelativePosition(extentNodeType, extentOffset)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getCurrentRelativeSelection = typeof getSelection !== 'undefined' ? _getCurrentRelativeSelection : domBinding => null
|
|
||||||
|
|
||||||
export function beforeTransactionSelectionFixer (domBinding) {
|
|
||||||
relativeSelection = getCurrentRelativeSelection(domBinding)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reset the browser range after every transaction.
|
|
||||||
* This prevents any collapsing issues with the local selection.
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
export function afterTransactionSelectionFixer (domBinding) {
|
|
||||||
if (relativeSelection !== null) {
|
|
||||||
domBinding.restoreSelection(relativeSelection)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
/* eslint-env browser */
|
|
||||||
/* global getSelection */
|
|
||||||
|
|
||||||
import YXmlText from '../../Types/YXml/YXmlText.js'
|
|
||||||
import YXmlHook from '../../Types/YXml/YXmlHook.js'
|
|
||||||
import { removeDomChildrenUntilElementFound } from './util.js'
|
|
||||||
|
|
||||||
function findScrollReference (scrollingElement) {
|
|
||||||
if (scrollingElement !== null) {
|
|
||||||
let anchor = getSelection().anchorNode
|
|
||||||
if (anchor == null) {
|
|
||||||
let children = scrollingElement.children // only iterate through non-text nodes
|
|
||||||
for (let i = 0; i < children.length; i++) {
|
|
||||||
const elem = children[i]
|
|
||||||
const rect = elem.getBoundingClientRect()
|
|
||||||
if (rect.top >= 0) {
|
|
||||||
return { elem, top: rect.top }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
/**
|
|
||||||
* @type {Element}
|
|
||||||
*/
|
|
||||||
let elem = anchor.parentElement
|
|
||||||
if (anchor instanceof Element) {
|
|
||||||
elem = anchor
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
elem,
|
|
||||||
top: elem.getBoundingClientRect().top
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
function fixScroll (scrollingElement, ref) {
|
|
||||||
if (ref !== null) {
|
|
||||||
const { elem, top } = ref
|
|
||||||
const currentTop = elem.getBoundingClientRect().top
|
|
||||||
const newScroll = scrollingElement.scrollTop + currentTop - top
|
|
||||||
if (newScroll >= 0) {
|
|
||||||
scrollingElement.scrollTop = newScroll
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
export default function typeObserver (events) {
|
|
||||||
this._mutualExclude(() => {
|
|
||||||
const scrollRef = findScrollReference(this.scrollingElement)
|
|
||||||
events.forEach(event => {
|
|
||||||
const yxml = event.target
|
|
||||||
const dom = this.typeToDom.get(yxml)
|
|
||||||
if (dom !== undefined && dom !== false) {
|
|
||||||
if (yxml.constructor === YXmlText) {
|
|
||||||
dom.nodeValue = yxml.toString()
|
|
||||||
} else if (event.attributesChanged !== undefined) {
|
|
||||||
// update attributes
|
|
||||||
event.attributesChanged.forEach(attributeName => {
|
|
||||||
const value = yxml.getAttribute(attributeName)
|
|
||||||
if (value === undefined) {
|
|
||||||
dom.removeAttribute(attributeName)
|
|
||||||
} else {
|
|
||||||
dom.setAttribute(attributeName, value)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
/*
|
|
||||||
* TODO: instead of hard-checking the types, it would be best to
|
|
||||||
* specify the type's features. E.g.
|
|
||||||
* - _yxmlHasAttributes
|
|
||||||
* - _yxmlHasChildren
|
|
||||||
* Furthermore, the features shouldn't be encoded in the types,
|
|
||||||
* only in the attributes (above)
|
|
||||||
*/
|
|
||||||
if (event.childListChanged && yxml.constructor !== YXmlHook) {
|
|
||||||
let currentChild = dom.firstChild
|
|
||||||
yxml.forEach(childType => {
|
|
||||||
const childNode = this.typeToDom.get(childType)
|
|
||||||
switch (childNode) {
|
|
||||||
case undefined:
|
|
||||||
// Does not exist. Create it.
|
|
||||||
const node = childType.toDom(this.opts.document, this.opts.hooks, this)
|
|
||||||
dom.insertBefore(node, currentChild)
|
|
||||||
break
|
|
||||||
case false:
|
|
||||||
// nop
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
// Is already attached to the dom.
|
|
||||||
// Find it and remove all dom nodes in-between.
|
|
||||||
removeDomChildrenUntilElementFound(dom, currentChild, childNode)
|
|
||||||
currentChild = childNode.nextSibling
|
|
||||||
break
|
|
||||||
}
|
|
||||||
})
|
|
||||||
removeDomChildrenUntilElementFound(dom, currentChild, null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
fixScroll(this.scrollingElement, scrollRef)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
|
|
||||||
import domToType from './domToType.js'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {import('../../Types/YXml/YXmlText.js').default} YXmlText
|
|
||||||
* @typedef {import('../../Types/YXml/YXmlElement.js').default} YXmlElement
|
|
||||||
* @typedef {import('../../Types/YXml/YXmlHook.js').default} YXmlHook
|
|
||||||
* @typedef {import('./DomBinding.js').default} DomBinding
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Iterates items until an undeleted item is found.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
export function iterateUntilUndeleted (item) {
|
|
||||||
while (item !== null && item._deleted) {
|
|
||||||
item = item._right
|
|
||||||
}
|
|
||||||
return item
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes an association (the information that a DOM element belongs to a
|
|
||||||
* type).
|
|
||||||
*
|
|
||||||
* @param {DomBinding} domBinding The binding object
|
|
||||||
* @param {Element} dom The dom that is to be associated with type
|
|
||||||
* @param {YXmlElement|YXmlHook} type The type that is to be associated with dom
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export function removeAssociation (domBinding, dom, type) {
|
|
||||||
domBinding.domToType.delete(dom)
|
|
||||||
domBinding.typeToDom.delete(type)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates an association (the information that a DOM element belongs to a
|
|
||||||
* type).
|
|
||||||
*
|
|
||||||
* @param {DomBinding} domBinding The binding object
|
|
||||||
* @param {DocumentFragment|Element|Text} dom The dom that is to be associated with type
|
|
||||||
* @param {YXmlElement|YXmlHook|YXmlText} type The type that is to be associated with dom
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export function createAssociation (domBinding, dom, type) {
|
|
||||||
if (domBinding !== undefined) {
|
|
||||||
domBinding.domToType.set(dom, type)
|
|
||||||
domBinding.typeToDom.set(type, dom)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If oldDom is associated with a type, associate newDom with the type and
|
|
||||||
* forget about oldDom. If oldDom is not associated with any type, nothing happens.
|
|
||||||
*
|
|
||||||
* @param {DomBinding} domBinding The binding object
|
|
||||||
* @param {Element} oldDom The existing dom
|
|
||||||
* @param {Element} newDom The new dom object
|
|
||||||
*/
|
|
||||||
export function switchAssociation (domBinding, oldDom, newDom) {
|
|
||||||
if (domBinding !== undefined) {
|
|
||||||
const type = domBinding.domToType.get(oldDom)
|
|
||||||
if (type !== undefined) {
|
|
||||||
removeAssociation(domBinding, oldDom, type)
|
|
||||||
createAssociation(domBinding, newDom, type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Insert Dom Elements after one of the children of this YXmlFragment.
|
|
||||||
* The Dom elements will be bound to a new YXmlElement and inserted at the
|
|
||||||
* specified position.
|
|
||||||
*
|
|
||||||
* @param {YXmlElement} type The type in which to insert DOM elements.
|
|
||||||
* @param {YXmlElement|null} prev The reference node. New YxmlElements are
|
|
||||||
* inserted after this node. Set null to insert at
|
|
||||||
* the beginning.
|
|
||||||
* @param {Array<Element>} doms The Dom elements to insert.
|
|
||||||
* @param {?Document} _document Optional. Provide the global document object.
|
|
||||||
* @param {DomBinding} binding The dom binding
|
|
||||||
* @return {Array<YXmlElement>} The YxmlElements that are inserted.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
export function insertDomElementsAfter (type, prev, doms, _document, binding) {
|
|
||||||
const types = domsToTypes(doms, _document, binding.opts.hooks, binding.filter, binding)
|
|
||||||
return type.insertAfter(prev, types)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function domsToTypes (doms, _document, hooks, filter, binding) {
|
|
||||||
const types = []
|
|
||||||
for (let dom of doms) {
|
|
||||||
const t = domToType(dom, _document, hooks, filter, binding)
|
|
||||||
if (t !== false) {
|
|
||||||
types.push(t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return types
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
export function insertNodeHelper (yxml, prevExpectedNode, child, _document, binding) {
|
|
||||||
let insertedNodes = insertDomElementsAfter(yxml, prevExpectedNode, [child], _document, binding)
|
|
||||||
if (insertedNodes.length > 0) {
|
|
||||||
return insertedNodes[0]
|
|
||||||
} else {
|
|
||||||
return prevExpectedNode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove children until `elem` is found.
|
|
||||||
*
|
|
||||||
* @param {Element} parent The parent of `elem` and `currentChild`.
|
|
||||||
* @param {Element} currentChild Start removing elements with `currentChild`. If
|
|
||||||
* `currentChild` is `elem` it won't be removed.
|
|
||||||
* @param {Element|null} elem The elemnt to look for.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
export function removeDomChildrenUntilElementFound (parent, currentChild, elem) {
|
|
||||||
while (currentChild !== elem) {
|
|
||||||
const del = currentChild
|
|
||||||
currentChild = currentChild.nextSibling
|
|
||||||
parent.removeChild(del)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import Binding from '../Binding.js'
|
|
||||||
|
|
||||||
function typeObserver (event) {
|
|
||||||
const quill = this.target
|
|
||||||
// Force flush Quill changes.
|
|
||||||
quill.update('yjs')
|
|
||||||
this._mutualExclude(function () {
|
|
||||||
// Apply computed delta.
|
|
||||||
quill.updateContents(event.delta, 'yjs')
|
|
||||||
// Force flush Quill changes. Ignore applied changes.
|
|
||||||
quill.update('yjs')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function quillObserver (delta) {
|
|
||||||
this._mutualExclude(() => {
|
|
||||||
this.type.applyDelta(delta.ops)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A Binding that binds a YText type to a Quill editor.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* const quill = new Quill(document.createElement('div'))
|
|
||||||
* const type = y.define('quill', Y.Text)
|
|
||||||
* const binding = new Y.QuillBinding(quill, type)
|
|
||||||
* // Now modifications on the DOM will be reflected in the Type, and the other
|
|
||||||
* // way around!
|
|
||||||
*/
|
|
||||||
export default class QuillBinding extends Binding {
|
|
||||||
/**
|
|
||||||
* @param {YText} textType
|
|
||||||
* @param {Quill} quill
|
|
||||||
*/
|
|
||||||
constructor (textType, quill) {
|
|
||||||
// Binding handles textType as this.type and quill as this.target.
|
|
||||||
super(textType, quill)
|
|
||||||
// Set initial value.
|
|
||||||
quill.setContents(textType.toDelta(), 'yjs')
|
|
||||||
// Observers are handled by this class.
|
|
||||||
this._typeObserver = typeObserver.bind(this)
|
|
||||||
this._quillObserver = quillObserver.bind(this)
|
|
||||||
textType.observe(this._typeObserver)
|
|
||||||
quill.on('text-change', this._quillObserver)
|
|
||||||
}
|
|
||||||
destroy () {
|
|
||||||
// Remove everything that is handled by this class.
|
|
||||||
this.type.unobserve(this._typeObserver)
|
|
||||||
this.target.off('text-change', this._quillObserver)
|
|
||||||
super.destroy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
|
|
||||||
import Binding from '../Binding.js'
|
|
||||||
import simpleDiff from '../../../lib/simpleDiff.js'
|
|
||||||
import { getRelativePosition, fromRelativePosition } from '../../Util/relativePosition.js'
|
|
||||||
|
|
||||||
function typeObserver () {
|
|
||||||
this._mutualExclude(() => {
|
|
||||||
const textarea = this.target
|
|
||||||
const textType = this.type
|
|
||||||
const relativeStart = getRelativePosition(textType, textarea.selectionStart)
|
|
||||||
const relativeEnd = getRelativePosition(textType, textarea.selectionEnd)
|
|
||||||
textarea.value = textType.toString()
|
|
||||||
const start = fromRelativePosition(textType._y, relativeStart)
|
|
||||||
const end = fromRelativePosition(textType._y, relativeEnd)
|
|
||||||
textarea.setSelectionRange(start, end)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function domObserver () {
|
|
||||||
this._mutualExclude(() => {
|
|
||||||
let diff = simpleDiff(this.type.toString(), this.target.value)
|
|
||||||
this.type.delete(diff.pos, diff.remove)
|
|
||||||
this.type.insert(diff.pos, diff.insert)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A binding that binds a YText to a dom textarea.
|
|
||||||
*
|
|
||||||
* This binding is automatically destroyed when its parent is deleted.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* const textare = document.createElement('textarea')
|
|
||||||
* const type = y.define('textarea', Y.Text)
|
|
||||||
* const binding = new Y.QuillBinding(type, textarea)
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export default class TextareaBinding extends Binding {
|
|
||||||
constructor (textType, domTextarea) {
|
|
||||||
// Binding handles textType as this.type and domTextarea as this.target
|
|
||||||
super(textType, domTextarea)
|
|
||||||
// set initial value
|
|
||||||
domTextarea.value = textType.toString()
|
|
||||||
// Observers are handled by this class
|
|
||||||
this._typeObserver = typeObserver.bind(this)
|
|
||||||
this._domObserver = domObserver.bind(this)
|
|
||||||
textType.observe(this._typeObserver)
|
|
||||||
domTextarea.addEventListener('input', this._domObserver)
|
|
||||||
}
|
|
||||||
destroy () {
|
|
||||||
// Remove everything that is handled by this class
|
|
||||||
this.type.unobserve(this._typeObserver)
|
|
||||||
this.target.unobserve(this._domObserver)
|
|
||||||
super.destroy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
294
src/Connector.js
Normal file
294
src/Connector.js
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
import BinaryEncoder from './Binary/Encoder.js'
|
||||||
|
import BinaryDecoder from './Binary/Decoder.js'
|
||||||
|
|
||||||
|
import { sendSyncStep1, readSyncStep1 } from './MessageHandler/syncStep1.js'
|
||||||
|
import { readSyncStep2 } from './MessageHandler/syncStep2.js'
|
||||||
|
import { integrateRemoteStructs } from './MessageHandler/integrateRemoteStructs.js'
|
||||||
|
|
||||||
|
import debug from 'debug'
|
||||||
|
|
||||||
|
export default class AbstractConnector {
|
||||||
|
constructor (y, opts) {
|
||||||
|
this.y = y
|
||||||
|
this.opts = opts
|
||||||
|
if (opts.role == null || opts.role === 'master') {
|
||||||
|
this.role = 'master'
|
||||||
|
} else if (opts.role === 'slave') {
|
||||||
|
this.role = 'slave'
|
||||||
|
} else {
|
||||||
|
throw new Error("Role must be either 'master' or 'slave'!")
|
||||||
|
}
|
||||||
|
this.log = debug('y:connector')
|
||||||
|
this.logMessage = debug('y:connector-message')
|
||||||
|
this._forwardAppliedStructs = opts.forwardAppliedOperations || false // TODO: rename
|
||||||
|
this.role = opts.role
|
||||||
|
this.connections = new Map()
|
||||||
|
this.isSynced = false
|
||||||
|
this.userEventListeners = []
|
||||||
|
this.whenSyncedListeners = []
|
||||||
|
this.currentSyncTarget = null
|
||||||
|
this.debug = opts.debug === true
|
||||||
|
this.broadcastBuffer = new BinaryEncoder()
|
||||||
|
this.broadcastBufferSize = 0
|
||||||
|
this.protocolVersion = 11
|
||||||
|
this.authInfo = opts.auth || null
|
||||||
|
this.checkAuth = opts.checkAuth || function () { return Promise.resolve('write') } // default is everyone has write access
|
||||||
|
if (opts.maxBufferLength == null) {
|
||||||
|
this.maxBufferLength = -1
|
||||||
|
} else {
|
||||||
|
this.maxBufferLength = opts.maxBufferLength
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reconnect () {
|
||||||
|
this.log('reconnecting..')
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect () {
|
||||||
|
this.log('discronnecting..')
|
||||||
|
this.connections = new Map()
|
||||||
|
this.isSynced = false
|
||||||
|
this.currentSyncTarget = null
|
||||||
|
this.whenSyncedListeners = []
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
onUserEvent (f) {
|
||||||
|
this.userEventListeners.push(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
removeUserEventListener (f) {
|
||||||
|
this.userEventListeners = this.userEventListeners.filter(g => f !== g)
|
||||||
|
}
|
||||||
|
|
||||||
|
userLeft (user) {
|
||||||
|
if (this.connections.has(user)) {
|
||||||
|
this.log('%s: User left %s', this.y.userID, user)
|
||||||
|
this.connections.delete(user)
|
||||||
|
// check if isSynced event can be sent now
|
||||||
|
this._setSyncedWith(null)
|
||||||
|
for (var f of this.userEventListeners) {
|
||||||
|
f({
|
||||||
|
action: 'userLeft',
|
||||||
|
user: user
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
userJoined (user, role, auth) {
|
||||||
|
if (role == null) {
|
||||||
|
throw new Error('You must specify the role of the joined user!')
|
||||||
|
}
|
||||||
|
if (this.connections.has(user)) {
|
||||||
|
throw new Error('This user already joined!')
|
||||||
|
}
|
||||||
|
this.log('%s: User joined %s', this.y.userID, user)
|
||||||
|
this.connections.set(user, {
|
||||||
|
uid: user,
|
||||||
|
isSynced: false,
|
||||||
|
role: role,
|
||||||
|
processAfterAuth: [],
|
||||||
|
processAfterSync: [],
|
||||||
|
auth: auth || null,
|
||||||
|
receivedSyncStep2: false
|
||||||
|
})
|
||||||
|
let defer = {}
|
||||||
|
defer.promise = new Promise(function (resolve) { defer.resolve = resolve })
|
||||||
|
this.connections.get(user).syncStep2 = defer
|
||||||
|
for (var f of this.userEventListeners) {
|
||||||
|
f({
|
||||||
|
action: 'userJoined',
|
||||||
|
user: user,
|
||||||
|
role: role
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this._syncWithUser(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute a function _when_ we are connected.
|
||||||
|
// If not connected, wait until connected
|
||||||
|
whenSynced (f) {
|
||||||
|
if (this.isSynced) {
|
||||||
|
f()
|
||||||
|
} else {
|
||||||
|
this.whenSyncedListeners.push(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_syncWithUser (userID) {
|
||||||
|
if (this.role === 'slave') {
|
||||||
|
return // "The current sync has not finished or this is controlled by a master!"
|
||||||
|
}
|
||||||
|
sendSyncStep1(this, userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
_fireIsSyncedListeners () {
|
||||||
|
if (!this.isSynced) {
|
||||||
|
this.isSynced = true
|
||||||
|
// It is safer to remove this!
|
||||||
|
// call whensynced listeners
|
||||||
|
for (var f of this.whenSyncedListeners) {
|
||||||
|
f()
|
||||||
|
}
|
||||||
|
this.whenSyncedListeners = []
|
||||||
|
this.y.emit('synced')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
send (uid, buffer) {
|
||||||
|
const y = this.y
|
||||||
|
if (!(buffer instanceof ArrayBuffer || buffer instanceof Uint8Array)) {
|
||||||
|
throw new Error('Expected Message to be an ArrayBuffer or Uint8Array - don\'t use this method to send custom messages')
|
||||||
|
}
|
||||||
|
this.log('User%s to User%s: Send \'%y\'', y.userID, uid, buffer)
|
||||||
|
this.logMessage('User%s to User%s: Send %Y', y.userID, uid, [y, buffer])
|
||||||
|
}
|
||||||
|
|
||||||
|
broadcast (buffer) {
|
||||||
|
const y = this.y
|
||||||
|
if (!(buffer instanceof ArrayBuffer || buffer instanceof Uint8Array)) {
|
||||||
|
throw new Error('Expected Message to be an ArrayBuffer or Uint8Array - don\'t use this method to send custom messages')
|
||||||
|
}
|
||||||
|
this.log('User%s: Broadcast \'%y\'', y.userID, buffer)
|
||||||
|
this.logMessage('User%s: Broadcast: %Y', y.userID, [y, buffer])
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Buffer operations, and broadcast them when ready.
|
||||||
|
*/
|
||||||
|
broadcastStruct (struct) {
|
||||||
|
const firstContent = this.broadcastBuffer.length === 0
|
||||||
|
if (firstContent) {
|
||||||
|
this.broadcastBuffer.writeVarString(this.y.room)
|
||||||
|
this.broadcastBuffer.writeVarString('update')
|
||||||
|
this.broadcastBufferSize = 0
|
||||||
|
this.broadcastBufferSizePos = this.broadcastBuffer.pos
|
||||||
|
this.broadcastBuffer.writeUint32(0)
|
||||||
|
}
|
||||||
|
this.broadcastBufferSize++
|
||||||
|
struct._toBinary(this.broadcastBuffer)
|
||||||
|
if (this.maxBufferLength > 0 && this.broadcastBuffer.length > this.maxBufferLength) {
|
||||||
|
// it is necessary to send the buffer now
|
||||||
|
// cache the buffer and check if server is responsive
|
||||||
|
const buffer = this.broadcastBuffer
|
||||||
|
buffer.setUint32(this.broadcastBufferSizePos, this.broadcastBufferSize)
|
||||||
|
this.broadcastBuffer = new BinaryEncoder()
|
||||||
|
this.whenRemoteResponsive().then(() => {
|
||||||
|
this.broadcast(buffer.createBuffer())
|
||||||
|
})
|
||||||
|
} else if (firstContent) {
|
||||||
|
// send the buffer when all transactions are finished
|
||||||
|
// (or buffer exceeds maxBufferLength)
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.broadcastBuffer.length > 0) {
|
||||||
|
const buffer = this.broadcastBuffer
|
||||||
|
buffer.setUint32(this.broadcastBufferSizePos, this.broadcastBufferSize)
|
||||||
|
this.broadcast(buffer.createBuffer())
|
||||||
|
this.broadcastBuffer = new BinaryEncoder()
|
||||||
|
}
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
receiveMessage (sender, buffer, skipAuth) {
|
||||||
|
const y = this.y
|
||||||
|
const userID = y.userID
|
||||||
|
skipAuth = skipAuth || false
|
||||||
|
if (!(buffer instanceof ArrayBuffer || buffer instanceof Uint8Array)) {
|
||||||
|
return Promise.reject(new Error('Expected Message to be an ArrayBuffer or Uint8Array!'))
|
||||||
|
}
|
||||||
|
if (sender === userID) {
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
let decoder = new BinaryDecoder(buffer)
|
||||||
|
let encoder = new BinaryEncoder()
|
||||||
|
let roomname = decoder.readVarString() // read room name
|
||||||
|
encoder.writeVarString(roomname)
|
||||||
|
let messageType = decoder.readVarString()
|
||||||
|
let senderConn = this.connections.get(sender)
|
||||||
|
this.log('User%s from User%s: Receive \'%s\'', userID, sender, messageType)
|
||||||
|
this.logMessage('User%s from User%s: Receive %Y', userID, sender, [y, buffer])
|
||||||
|
if (senderConn == null && !skipAuth) {
|
||||||
|
throw new Error('Received message from unknown peer!')
|
||||||
|
}
|
||||||
|
if (messageType === 'sync step 1' || messageType === 'sync step 2') {
|
||||||
|
let auth = decoder.readVarUint()
|
||||||
|
if (senderConn.auth == null) {
|
||||||
|
senderConn.processAfterAuth.push([messageType, senderConn, decoder, encoder, sender])
|
||||||
|
// check auth
|
||||||
|
return this.checkAuth(auth, y, sender).then(authPermissions => {
|
||||||
|
if (senderConn.auth == null) {
|
||||||
|
senderConn.auth = authPermissions
|
||||||
|
y.emit('userAuthenticated', {
|
||||||
|
user: senderConn.uid,
|
||||||
|
auth: authPermissions
|
||||||
|
})
|
||||||
|
}
|
||||||
|
let messages = senderConn.processAfterAuth
|
||||||
|
senderConn.processAfterAuth = []
|
||||||
|
|
||||||
|
messages.forEach(m =>
|
||||||
|
this.computeMessage(m[0], m[1], m[2], m[3], m[4])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((skipAuth || senderConn.auth != null) && (messageType !== 'update' || senderConn.isSynced)) {
|
||||||
|
this.computeMessage(messageType, senderConn, decoder, encoder, sender, skipAuth)
|
||||||
|
} else {
|
||||||
|
senderConn.processAfterSync.push([messageType, senderConn, decoder, encoder, sender, false])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
computeMessage (messageType, senderConn, decoder, encoder, sender, skipAuth) {
|
||||||
|
if (messageType === 'sync step 1' && (senderConn.auth === 'write' || senderConn.auth === 'read')) {
|
||||||
|
// cannot wait for sync step 1 to finish, because we may wait for sync step 2 in sync step 1 (->lock)
|
||||||
|
readSyncStep1(decoder, encoder, this.y, senderConn, sender)
|
||||||
|
} else {
|
||||||
|
const y = this.y
|
||||||
|
y.transact(function () {
|
||||||
|
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)
|
||||||
|
} else {
|
||||||
|
throw new Error('Unable to receive message')
|
||||||
|
}
|
||||||
|
}, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_setSyncedWith (user) {
|
||||||
|
if (user != null) {
|
||||||
|
const userConn = this.connections.get(user)
|
||||||
|
userConn.isSynced = true
|
||||||
|
const messages = userConn.processAfterSync
|
||||||
|
userConn.processAfterSync = []
|
||||||
|
messages.forEach(m => {
|
||||||
|
this.computeMessage(m[0], m[1], m[2], m[3], m[4])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const conns = Array.from(this.connections.values())
|
||||||
|
if (conns.length > 0 && conns.every(u => u.isSynced)) {
|
||||||
|
this._fireIsSyncedListeners()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
130
src/MessageHandler/deleteSet.js
Normal file
130
src/MessageHandler/deleteSet.js
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import { deleteItemRange } from '../Struct/Delete.js'
|
||||||
|
import ID from '../Util/ID.js'
|
||||||
|
|
||||||
|
export function stringifyDeleteSet (y, decoder, strBuilder) {
|
||||||
|
let dsLength = decoder.readUint32()
|
||||||
|
for (let i = 0; i < dsLength; i++) {
|
||||||
|
let user = decoder.readVarUint()
|
||||||
|
strBuilder.push(' -' + user + ':')
|
||||||
|
let dvLength = decoder.readVarUint()
|
||||||
|
for (let j = 0; j < dvLength; j++) {
|
||||||
|
let from = decoder.readVarUint()
|
||||||
|
let len = decoder.readVarUint()
|
||||||
|
let gc = decoder.readUint8() === 1
|
||||||
|
strBuilder.push(`clock: ${from}, length: ${len}, gc: ${gc}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strBuilder
|
||||||
|
}
|
||||||
|
|
||||||
|
export function writeDeleteSet (y, encoder) {
|
||||||
|
let currentUser = null
|
||||||
|
let currentLength
|
||||||
|
let lastLenPos
|
||||||
|
|
||||||
|
let numberOfUsers = 0
|
||||||
|
let laterDSLenPus = encoder.pos
|
||||||
|
encoder.writeUint32(0)
|
||||||
|
|
||||||
|
y.ds.iterate(null, null, function (n) {
|
||||||
|
var user = n._id.user
|
||||||
|
var clock = n._id.clock
|
||||||
|
var len = n.len
|
||||||
|
var gc = n.gc
|
||||||
|
if (currentUser !== user) {
|
||||||
|
numberOfUsers++
|
||||||
|
// a new user was found
|
||||||
|
if (currentUser !== null) { // happens on first iteration
|
||||||
|
encoder.setUint32(lastLenPos, currentLength)
|
||||||
|
}
|
||||||
|
currentUser = user
|
||||||
|
encoder.writeVarUint(user)
|
||||||
|
// pseudo-fill pos
|
||||||
|
lastLenPos = encoder.pos
|
||||||
|
encoder.writeUint32(0)
|
||||||
|
currentLength = 0
|
||||||
|
}
|
||||||
|
encoder.writeVarUint(clock)
|
||||||
|
encoder.writeVarUint(len)
|
||||||
|
encoder.writeUint8(gc ? 1 : 0)
|
||||||
|
currentLength++
|
||||||
|
})
|
||||||
|
if (currentUser !== null) { // happens on first iteration
|
||||||
|
encoder.setUint32(lastLenPos, currentLength)
|
||||||
|
}
|
||||||
|
encoder.setUint32(laterDSLenPus, numberOfUsers)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readDeleteSet (y, decoder) {
|
||||||
|
let dsLength = decoder.readUint32()
|
||||||
|
for (let i = 0; i < dsLength; i++) {
|
||||||
|
let user = decoder.readVarUint()
|
||||||
|
let dv = []
|
||||||
|
let dvLength = decoder.readUint32()
|
||||||
|
for (let j = 0; j < dvLength; j++) {
|
||||||
|
let from = decoder.readVarUint()
|
||||||
|
let len = decoder.readVarUint()
|
||||||
|
let gc = decoder.readUint8() === 1
|
||||||
|
dv.push([from, len, gc])
|
||||||
|
}
|
||||||
|
if (dvLength > 0) {
|
||||||
|
let pos = 0
|
||||||
|
let d = dv[pos]
|
||||||
|
let deletions = []
|
||||||
|
y.ds.iterate(new ID(user, 0), new ID(user, Number.MAX_VALUE), function (n) {
|
||||||
|
// cases:
|
||||||
|
// 1. d deletes something to the right of n
|
||||||
|
// => go to next n (break)
|
||||||
|
// 2. d deletes something to the left of n
|
||||||
|
// => create deletions
|
||||||
|
// => reset d accordingly
|
||||||
|
// *)=> if d doesn't delete anything anymore, go to next d (continue)
|
||||||
|
// 3. not 2) and d deletes something that also n deletes
|
||||||
|
// => reset d so that it doesn't contain n's deletion
|
||||||
|
// *)=> if d does not delete anything anymore, go to next d (continue)
|
||||||
|
while (d != null) {
|
||||||
|
var diff = 0 // describe the diff of length in 1) and 2)
|
||||||
|
if (n._id.clock + n.len <= d[0]) {
|
||||||
|
// 1)
|
||||||
|
break
|
||||||
|
} else if (d[0] < n._id.clock) {
|
||||||
|
// 2)
|
||||||
|
// delete maximum the len of d
|
||||||
|
// else delete as much as possible
|
||||||
|
diff = Math.min(n._id.clock - d[0], d[1])
|
||||||
|
// deleteItemRange(y, user, d[0], diff)
|
||||||
|
deletions.push([user, d[0], diff])
|
||||||
|
} else {
|
||||||
|
// 3)
|
||||||
|
diff = n._id.clock + n.len - d[0] // never null (see 1)
|
||||||
|
if (d[2] && !n.gc) {
|
||||||
|
// d marks as gc'd but n does not
|
||||||
|
// then delete either way
|
||||||
|
// deleteItemRange(y, user, d[0], Math.min(diff, d[1]))
|
||||||
|
deletions.push([user, d[0], Math.min(diff, d[1])])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (d[1] <= diff) {
|
||||||
|
// d doesn't delete anything anymore
|
||||||
|
d = dv[++pos]
|
||||||
|
} else {
|
||||||
|
d[0] = d[0] + diff // reset pos
|
||||||
|
d[1] = d[1] - diff // reset length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// TODO: It would be more performant to apply the deletes in the above loop
|
||||||
|
// Adapt the Tree implementation to support delete while iterating
|
||||||
|
for (let i = deletions.length - 1; i >= 0; i--) {
|
||||||
|
const del = deletions[i]
|
||||||
|
deleteItemRange(y, del[0], del[1], del[2])
|
||||||
|
}
|
||||||
|
// for the rest.. just apply it
|
||||||
|
for (; pos < dv.length; pos++) {
|
||||||
|
d = dv[pos]
|
||||||
|
deleteItemRange(y, user, d[0], d[1])
|
||||||
|
// deletions.push([user, d[0], d[1], d[2]])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,6 @@
|
|||||||
import { getStruct } from '../Util/structReferences.js'
|
import { getStruct } from '../Util/structReferences.js'
|
||||||
import * as decoding from '../../lib/decoding.js'
|
import BinaryDecoder from '../Binary/Decoder.js'
|
||||||
import GC from '../Struct/GC.js'
|
import { logID } from './messageToString.js'
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {import('../index').Y} Y
|
|
||||||
* @typedef {import('../Struct/Item.js').default} YItem
|
|
||||||
*/
|
|
||||||
|
|
||||||
class MissingEntry {
|
class MissingEntry {
|
||||||
constructor (decoder, missing, struct) {
|
constructor (decoder, missing, struct) {
|
||||||
@@ -16,12 +11,9 @@ class MissingEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
|
||||||
* Integrate remote struct
|
* Integrate remote struct
|
||||||
* When a remote struct is integrated, other structs might be ready to ready to
|
* When a remote struct is integrated, other structs might be ready to ready to
|
||||||
* integrate.
|
* integrate.
|
||||||
* @param {Y} y
|
|
||||||
* @param {YItem} struct
|
|
||||||
*/
|
*/
|
||||||
function _integrateRemoteStructHelper (y, struct) {
|
function _integrateRemoteStructHelper (y, struct) {
|
||||||
const id = struct._id
|
const id = struct._id
|
||||||
@@ -31,14 +23,7 @@ function _integrateRemoteStructHelper (y, struct) {
|
|||||||
if (y.ss.getState(id.user) > id.clock) {
|
if (y.ss.getState(id.user) > id.clock) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!y.gcEnabled || struct.constructor === GC || (struct._parent.constructor !== GC && struct._parent._deleted === false)) {
|
struct._integrate(y)
|
||||||
// Is either a GC or Item with an undeleted parent
|
|
||||||
// save to integrate
|
|
||||||
struct._integrate(y)
|
|
||||||
} else {
|
|
||||||
// Is an Item. parent was deleted.
|
|
||||||
struct._gc(y)
|
|
||||||
}
|
|
||||||
let msu = y._missingStructs.get(id.user)
|
let msu = y._missingStructs.get(id.user)
|
||||||
if (msu != null) {
|
if (msu != null) {
|
||||||
let clock = id.clock
|
let clock = id.clock
|
||||||
@@ -55,29 +40,35 @@ function _integrateRemoteStructHelper (y, struct) {
|
|||||||
decoder.pos = oldPos
|
decoder.pos = oldPos
|
||||||
if (missing.length === 0) {
|
if (missing.length === 0) {
|
||||||
y._readyToIntegrate.push(missingDef.struct)
|
y._readyToIntegrate.push(missingDef.struct)
|
||||||
} else {
|
|
||||||
// TODO: throw error here
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
msu.delete(clock)
|
msu.delete(clock)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (msu.size === 0) {
|
|
||||||
y._missingStructs.delete(id.user)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export function stringifyStructs (y, decoder, strBuilder) {
|
||||||
* @param {decoding.Decoder} decoder
|
const len = decoder.readUint32()
|
||||||
* @param {Y} y
|
|
||||||
*/
|
|
||||||
export function integrateRemoteStructs (decoder, y) {
|
|
||||||
const len = decoding.readUint32(decoder)
|
|
||||||
for (let i = 0; i < len; i++) {
|
for (let i = 0; i < len; i++) {
|
||||||
let reference = decoding.readVarUint(decoder)
|
let reference = decoder.readVarUint()
|
||||||
|
let Constr = getStruct(reference)
|
||||||
|
let struct = new Constr()
|
||||||
|
let missing = struct._fromBinary(y, decoder)
|
||||||
|
let logMessage = ' ' + struct._logString()
|
||||||
|
if (missing.length > 0) {
|
||||||
|
logMessage += ' .. missing: ' + missing.map(logID).join(', ')
|
||||||
|
}
|
||||||
|
strBuilder.push(logMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function integrateRemoteStructs (decoder, encoder, y) {
|
||||||
|
const len = decoder.readUint32()
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
let reference = decoder.readVarUint()
|
||||||
let Constr = getStruct(reference)
|
let Constr = getStruct(reference)
|
||||||
let struct = new Constr()
|
let struct = new Constr()
|
||||||
let decoderPos = decoder.pos
|
let decoderPos = decoder.pos
|
||||||
@@ -88,7 +79,7 @@ export function integrateRemoteStructs (decoder, y) {
|
|||||||
struct = y._readyToIntegrate.shift()
|
struct = y._readyToIntegrate.shift()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let _decoder = decoding.createDecoder(decoder.arr.buffer)
|
let _decoder = new BinaryDecoder(decoder.uint8arr)
|
||||||
_decoder.pos = decoderPos
|
_decoder.pos = decoderPos
|
||||||
let missingEntry = new MissingEntry(_decoder, missing, struct)
|
let missingEntry = new MissingEntry(_decoder, missing, struct)
|
||||||
let missingStructs = y._missingStructs
|
let missingStructs = y._missingStructs
|
||||||
@@ -107,39 +98,3 @@ export function integrateRemoteStructs (decoder, y) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: use this above / refactor
|
|
||||||
/**
|
|
||||||
* @param {decoding.Decoder} decoder
|
|
||||||
* @param {Y} y
|
|
||||||
*/
|
|
||||||
export function integrateRemoteStruct (decoder, y) {
|
|
||||||
let reference = decoding.readVarUint(decoder)
|
|
||||||
let Constr = getStruct(reference)
|
|
||||||
let struct = new Constr()
|
|
||||||
let decoderPos = decoder.pos
|
|
||||||
let missing = struct._fromBinary(y, decoder)
|
|
||||||
if (missing.length === 0) {
|
|
||||||
while (struct != null) {
|
|
||||||
_integrateRemoteStructHelper(y, struct)
|
|
||||||
struct = y._readyToIntegrate.shift()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let _decoder = decoding.createDecoder(decoder.arr.buffer)
|
|
||||||
_decoder.pos = decoderPos
|
|
||||||
let missingEntry = new MissingEntry(_decoder, missing, struct)
|
|
||||||
let missingStructs = y._missingStructs
|
|
||||||
for (let i = missing.length - 1; i >= 0; i--) {
|
|
||||||
let m = missing[i]
|
|
||||||
if (!missingStructs.has(m.user)) {
|
|
||||||
missingStructs.set(m.user, new Map())
|
|
||||||
}
|
|
||||||
let msu = missingStructs.get(m.user)
|
|
||||||
if (!msu.has(m.clock)) {
|
|
||||||
msu.set(m.clock, [])
|
|
||||||
}
|
|
||||||
let mArray = msu = msu.get(m.clock)
|
|
||||||
mArray.push(missingEntry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
48
src/MessageHandler/messageToString.js
Normal file
48
src/MessageHandler/messageToString.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import BinaryDecoder from '../Binary/Decoder.js'
|
||||||
|
import { stringifyStructs } from './integrateRemoteStructs.js'
|
||||||
|
import { stringifySyncStep1 } from './syncStep1.js'
|
||||||
|
import { stringifySyncStep2 } from './syncStep2.js'
|
||||||
|
import ID from '../Util/ID.js'
|
||||||
|
import RootID from '../Util/RootID.js'
|
||||||
|
import Y from '../Y.js'
|
||||||
|
|
||||||
|
export function messageToString ([y, buffer]) {
|
||||||
|
let decoder = new BinaryDecoder(buffer)
|
||||||
|
decoder.readVarString() // read roomname
|
||||||
|
let type = decoder.readVarString()
|
||||||
|
let strBuilder = []
|
||||||
|
strBuilder.push('\n === ' + type + ' ===')
|
||||||
|
if (type === 'update') {
|
||||||
|
stringifyStructs(y, decoder, strBuilder)
|
||||||
|
} else if (type === 'sync step 1') {
|
||||||
|
stringifySyncStep1(y, decoder, strBuilder)
|
||||||
|
} else if (type === 'sync step 2') {
|
||||||
|
stringifySyncStep2(y, decoder, strBuilder)
|
||||||
|
} else {
|
||||||
|
strBuilder.push('-- Unknown message type - probably an encoding issue!!!')
|
||||||
|
}
|
||||||
|
return strBuilder.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function messageToRoomname (buffer) {
|
||||||
|
let decoder = new BinaryDecoder(buffer)
|
||||||
|
decoder.readVarString() // roomname
|
||||||
|
return decoder.readVarString() // messageType
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logID (id) {
|
||||||
|
if (id !== null && id._id != null) {
|
||||||
|
id = id._id
|
||||||
|
}
|
||||||
|
if (id === null) {
|
||||||
|
return '()'
|
||||||
|
} else if (id instanceof ID) {
|
||||||
|
return `(${id.user},${id.clock})`
|
||||||
|
} else if (id instanceof RootID) {
|
||||||
|
return `(${id.name},${id.type})`
|
||||||
|
} else if (id.constructor === Y) {
|
||||||
|
return `y`
|
||||||
|
} else {
|
||||||
|
throw new Error('This is not a valid ID!')
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/MessageHandler/stateSet.js
Normal file
23
src/MessageHandler/stateSet.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
|
||||||
|
export function readStateSet (decoder) {
|
||||||
|
let ss = new Map()
|
||||||
|
let ssLength = decoder.readUint32()
|
||||||
|
for (let i = 0; i < ssLength; i++) {
|
||||||
|
let user = decoder.readVarUint()
|
||||||
|
let clock = decoder.readVarUint()
|
||||||
|
ss.set(user, clock)
|
||||||
|
}
|
||||||
|
return ss
|
||||||
|
}
|
||||||
|
|
||||||
|
export function writeStateSet (y, encoder) {
|
||||||
|
let lenPosition = encoder.pos
|
||||||
|
let len = 0
|
||||||
|
encoder.writeUint32(0)
|
||||||
|
for (let [user, clock] of y.ss.state) {
|
||||||
|
encoder.writeVarUint(user)
|
||||||
|
encoder.writeVarUint(clock)
|
||||||
|
len++
|
||||||
|
}
|
||||||
|
encoder.setUint32(lenPosition, len)
|
||||||
|
}
|
||||||
70
src/MessageHandler/syncStep1.js
Normal file
70
src/MessageHandler/syncStep1.js
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import BinaryEncoder from '../Binary/Encoder.js'
|
||||||
|
import { readStateSet, writeStateSet } from './stateSet.js'
|
||||||
|
import { writeDeleteSet } from './deleteSet.js'
|
||||||
|
import ID from '../Util/ID.js'
|
||||||
|
import { RootFakeUserID } from '../Util/RootID.js'
|
||||||
|
|
||||||
|
export function stringifySyncStep1 (y, decoder, strBuilder) {
|
||||||
|
let auth = decoder.readVarString()
|
||||||
|
let protocolVersion = decoder.readVarUint()
|
||||||
|
strBuilder.push(` - auth: "${auth}"`)
|
||||||
|
strBuilder.push(` - protocolVersion: ${protocolVersion}`)
|
||||||
|
// write SS
|
||||||
|
let ssBuilder = []
|
||||||
|
let len = decoder.readUint32()
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
let user = decoder.readVarUint()
|
||||||
|
let clock = decoder.readVarUint()
|
||||||
|
ssBuilder.push(`(${user}:${clock})`)
|
||||||
|
}
|
||||||
|
strBuilder.push(' == SS: ' + ssBuilder.join(','))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sendSyncStep1 (connector, syncUser) {
|
||||||
|
let encoder = new BinaryEncoder()
|
||||||
|
encoder.writeVarString(connector.y.room)
|
||||||
|
encoder.writeVarString('sync step 1')
|
||||||
|
encoder.writeVarString(connector.authInfo || '')
|
||||||
|
encoder.writeVarUint(connector.protocolVersion)
|
||||||
|
writeStateSet(connector.y, encoder)
|
||||||
|
connector.send(syncUser, encoder.createBuffer())
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function writeStructs (encoder, decoder, y, ss) {
|
||||||
|
const lenPos = encoder.pos
|
||||||
|
encoder.writeUint32(0)
|
||||||
|
let len = 0
|
||||||
|
for (let user of y.ss.state.keys()) {
|
||||||
|
let clock = ss.get(user) || 0
|
||||||
|
if (user !== RootFakeUserID) {
|
||||||
|
y.os.iterate(new ID(user, clock), new ID(user, Number.MAX_VALUE), function (struct) {
|
||||||
|
struct._toBinary(encoder)
|
||||||
|
len++
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
encoder.setUint32(lenPos, len)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readSyncStep1 (decoder, encoder, y, senderConn, sender) {
|
||||||
|
let protocolVersion = decoder.readVarUint()
|
||||||
|
// check protocol version
|
||||||
|
if (protocolVersion !== y.connector.protocolVersion) {
|
||||||
|
console.warn(
|
||||||
|
`You tried to sync with a Yjs instance that has a different protocol version
|
||||||
|
(You: ${protocolVersion}, Client: ${protocolVersion}).
|
||||||
|
`)
|
||||||
|
y.destroy()
|
||||||
|
}
|
||||||
|
// write sync step 2
|
||||||
|
encoder.writeVarString('sync step 2')
|
||||||
|
encoder.writeVarString(y.connector.authInfo || '')
|
||||||
|
const ss = readStateSet(decoder)
|
||||||
|
writeStructs(encoder, decoder, y, ss)
|
||||||
|
writeDeleteSet(y, encoder)
|
||||||
|
y.connector.send(senderConn.uid, encoder.createBuffer())
|
||||||
|
senderConn.receivedSyncStep2 = true
|
||||||
|
if (y.connector.role === 'slave') {
|
||||||
|
sendSyncStep1(y.connector, sender)
|
||||||
|
}
|
||||||
|
}
|
||||||
28
src/MessageHandler/syncStep2.js
Normal file
28
src/MessageHandler/syncStep2.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { stringifyStructs, integrateRemoteStructs } from './integrateRemoteStructs.js'
|
||||||
|
import { readDeleteSet } from './deleteSet.js'
|
||||||
|
|
||||||
|
export function stringifySyncStep2 (y, decoder, strBuilder) {
|
||||||
|
strBuilder.push(' - auth: ' + decoder.readVarString())
|
||||||
|
strBuilder.push(' == OS:')
|
||||||
|
stringifyStructs(y, decoder, strBuilder)
|
||||||
|
// write DS to string
|
||||||
|
strBuilder.push(' == DS:')
|
||||||
|
let len = decoder.readUint32()
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
let user = decoder.readVarUint()
|
||||||
|
strBuilder.push(` User: ${user}: `)
|
||||||
|
let len2 = decoder.readUint32()
|
||||||
|
for (let j = 0; j < len2; j++) {
|
||||||
|
let from = decoder.readVarUint()
|
||||||
|
let to = decoder.readVarUint()
|
||||||
|
let gc = decoder.readUint8() === 1
|
||||||
|
strBuilder.push(`[${from}, ${to}, ${gc}]`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readSyncStep2 (decoder, encoder, y, senderConn, sender) {
|
||||||
|
integrateRemoteStructs(decoder, encoder, y)
|
||||||
|
readDeleteSet(y, decoder)
|
||||||
|
y.connector._setSyncedWith(sender)
|
||||||
|
}
|
||||||
47
src/Persistence.js
Normal file
47
src/Persistence.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
// import BinaryEncoder from './Binary/Encoder.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')
|
||||||
|
}
|
||||||
|
|
||||||
|
saveToMessageQueue (binary) {
|
||||||
|
this.log('Room %s: Save message to message queue', this.y.options.connector.room)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
this.saveToMessageQueue(encoder.createBuffer())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
if (this.saveOperationsBuffer.length === 0) {
|
||||||
|
this.saveOperationsBuffer = ops
|
||||||
|
} else {
|
||||||
|
this.saveOperationsBuffer = this.saveOperationsBuffer.concat(ops)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Y.AbstractPersistence = AbstractPersistence
|
||||||
|
}
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
|
|
||||||
export default class AbstractPersistence {}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
import fs from 'fs'
|
|
||||||
import path from 'path'
|
|
||||||
import * as encoding from '../../lib/encoding.js'
|
|
||||||
import * as decoding from '../../lib/decoding.js'
|
|
||||||
import { createMutualExclude } from '../../lib/mutualExclude.js'
|
|
||||||
import { encodeUpdate, encodeStructsDS, decodePersisted } from './decodePersisted.js'
|
|
||||||
|
|
||||||
function createFilePath (persistence, roomName) {
|
|
||||||
// TODO: filename checking!
|
|
||||||
return path.join(persistence.dir, roomName)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class FilePersistence {
|
|
||||||
constructor (dir) {
|
|
||||||
this.dir = dir
|
|
||||||
this._mutex = createMutualExclude()
|
|
||||||
}
|
|
||||||
setRemoteUpdateCounter (roomName, remoteUpdateCounter) {
|
|
||||||
// TODO: implement
|
|
||||||
// nop
|
|
||||||
}
|
|
||||||
saveUpdate (room, y, encodedStructs) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this._mutex(() => {
|
|
||||||
const filePath = createFilePath(this, room)
|
|
||||||
const updateMessage = encoding.createEncoder()
|
|
||||||
encodeUpdate(y, encodedStructs, updateMessage)
|
|
||||||
fs.appendFile(filePath, Buffer.from(encoding.toBuffer(updateMessage)), (err) => {
|
|
||||||
if (err !== null) {
|
|
||||||
reject(err)
|
|
||||||
} else {
|
|
||||||
resolve()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}, resolve)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
saveState (roomName, y) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const encoder = encoding.createEncoder()
|
|
||||||
encodeStructsDS(y, encoder)
|
|
||||||
const filePath = createFilePath(this, roomName)
|
|
||||||
fs.writeFile(filePath, Buffer.from(encoding.toBuffer(encoder)), (err) => {
|
|
||||||
if (err !== null) {
|
|
||||||
reject(err)
|
|
||||||
} else {
|
|
||||||
resolve()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
readState (roomName, y) {
|
|
||||||
// Check if the file exists in the current directory.
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const filePath = path.join(this.dir, roomName)
|
|
||||||
fs.readFile(filePath, (err, data) => {
|
|
||||||
if (err !== null) {
|
|
||||||
resolve()
|
|
||||||
// reject(err)
|
|
||||||
} else {
|
|
||||||
this._mutex(() => {
|
|
||||||
console.info(`unpacking data (${data.length})`)
|
|
||||||
console.time('unpacking')
|
|
||||||
decodePersisted(y, decoding.createDecoder(data.buffer))
|
|
||||||
console.timeEnd('unpacking')
|
|
||||||
})
|
|
||||||
resolve()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,281 +0,0 @@
|
|||||||
/* global indexedDB, location, BroadcastChannel */
|
|
||||||
|
|
||||||
import Y from '../Y.js'
|
|
||||||
import { createMutualExclude } from '../../lib/mutualExclude.js'
|
|
||||||
import { decodePersisted, encodeStructsDS, encodeUpdate, PERSIST_STRUCTS_DS, PERSIST_UPDATE } from './decodePersisted.js'
|
|
||||||
import * as decoding from '../../lib/decoding.js'
|
|
||||||
import * as encoding from '../../lib/encoding.js'
|
|
||||||
/*
|
|
||||||
* Request to Promise transformer
|
|
||||||
*/
|
|
||||||
function rtop (request) {
|
|
||||||
return new Promise(function (resolve, reject) {
|
|
||||||
request.onerror = function (event) {
|
|
||||||
reject(new Error(event.target.error))
|
|
||||||
}
|
|
||||||
request.onblocked = function () {
|
|
||||||
location.reload()
|
|
||||||
}
|
|
||||||
request.onsuccess = function (event) {
|
|
||||||
resolve(event.target.result)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function openDB (room) {
|
|
||||||
return new Promise(function (resolve, reject) {
|
|
||||||
let request = indexedDB.open(room)
|
|
||||||
request.onupgradeneeded = function (event) {
|
|
||||||
const db = event.target.result
|
|
||||||
if (db.objectStoreNames.contains('updates')) {
|
|
||||||
db.deleteObjectStore('updates')
|
|
||||||
}
|
|
||||||
db.createObjectStore('updates', {autoIncrement: true})
|
|
||||||
}
|
|
||||||
request.onerror = function (event) {
|
|
||||||
reject(new Error(event.target.error))
|
|
||||||
}
|
|
||||||
request.onblocked = function () {
|
|
||||||
location.reload()
|
|
||||||
}
|
|
||||||
request.onsuccess = function (event) {
|
|
||||||
const db = event.target.result
|
|
||||||
db.onversionchange = function () { db.close() }
|
|
||||||
resolve(db)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function persist (room) {
|
|
||||||
let t = room.db.transaction(['updates'], 'readwrite')
|
|
||||||
let updatesStore = t.objectStore('updates')
|
|
||||||
return rtop(updatesStore.getAll())
|
|
||||||
.then(updates => {
|
|
||||||
// apply all previous updates before deleting them
|
|
||||||
room.mutex(() => {
|
|
||||||
updates.forEach(update => {
|
|
||||||
decodePersisted(y, new BinaryDecoder(update))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
const encoder = new BinaryEncoder()
|
|
||||||
encodeStructsDS(y, encoder)
|
|
||||||
// delete all pending updates
|
|
||||||
rtop(updatesStore.clear()).then(() => {
|
|
||||||
// write current model
|
|
||||||
updatesStore.put(encoder.createBuffer())
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveUpdate (room, updateBuffer) {
|
|
||||||
const db = room.db
|
|
||||||
if (db !== null) {
|
|
||||||
const t = db.transaction(['updates'], 'readwrite')
|
|
||||||
const updatesStore = t.objectStore('updates')
|
|
||||||
const updatePut = rtop(updatesStore.put(updateBuffer))
|
|
||||||
rtop(updatesStore.count()).then(cnt => {
|
|
||||||
if (cnt >= PREFERRED_TRIM_SIZE) {
|
|
||||||
persist(room)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return updatePut
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function registerRoomInPersistence (documentsDB, roomName) {
|
|
||||||
return documentsDB.then(
|
|
||||||
db => Promise.all([
|
|
||||||
db,
|
|
||||||
rtop(db.transaction(['documents'], 'readonly').objectStore('documents').get(roomName))
|
|
||||||
])
|
|
||||||
).then(
|
|
||||||
([db, doc]) => {
|
|
||||||
if (doc === undefined) {
|
|
||||||
return rtop(db.transaction(['documents'], 'readwrite').objectStore('documents').add({ roomName, serverUpdateCounter: 0 }))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const PREFERRED_TRIM_SIZE = 400
|
|
||||||
|
|
||||||
export default class IndexedDBPersistence {
|
|
||||||
constructor () {
|
|
||||||
this._rooms = new Map()
|
|
||||||
this._documentsDB = new Promise(function (resolve, reject) {
|
|
||||||
let request = indexedDB.open('_yjs_documents')
|
|
||||||
request.onupgradeneeded = function (event) {
|
|
||||||
const db = event.target.result
|
|
||||||
if (db.objectStoreNames.contains('documents')) {
|
|
||||||
db.deleteObjectStore('documents')
|
|
||||||
}
|
|
||||||
db.createObjectStore('documents', { keyPath: "roomName" })
|
|
||||||
}
|
|
||||||
request.onerror = function (event) {
|
|
||||||
reject(new Error(event.target.error))
|
|
||||||
}
|
|
||||||
request.onblocked = function () {
|
|
||||||
location.reload()
|
|
||||||
}
|
|
||||||
request.onsuccess = function (event) {
|
|
||||||
const db = event.target.result
|
|
||||||
db.onversionchange = function () { db.close() }
|
|
||||||
resolve(db)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
addEventListener('unload', () => {
|
|
||||||
// close everything when page unloads
|
|
||||||
this._rooms.forEach(room => {
|
|
||||||
if (room.db !== null) {
|
|
||||||
room.db.close()
|
|
||||||
} else {
|
|
||||||
room.dbPromise.then(db => db.close())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
this._documentsDB.then(db => db.close())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
getAllDocuments () {
|
|
||||||
return this._documentsDB.then(
|
|
||||||
db => rtop(db.transaction(['documents'], 'readonly').objectStore('documents').getAll())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
setRemoteUpdateCounter (roomName, remoteUpdateCounter) {
|
|
||||||
this._documentsDB.then(
|
|
||||||
db => rtop(db.transaction(['documents'], 'readwrite').objectStore('documents').put({ roomName, remoteUpdateCounter }))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
_createYInstance (roomName) {
|
|
||||||
const room = this._rooms.get(roomName)
|
|
||||||
if (room !== undefined) {
|
|
||||||
return room.y
|
|
||||||
}
|
|
||||||
const y = new Y()
|
|
||||||
return openDB(roomName).then(
|
|
||||||
db => rtop(db.transaction(['updates'], 'readonly').objectStore('updates').getAll())
|
|
||||||
).then(
|
|
||||||
updates =>
|
|
||||||
y.transact(() => {
|
|
||||||
updates.forEach(update => {
|
|
||||||
decodePersisted(y, new BinaryDecoder(update))
|
|
||||||
})
|
|
||||||
}, true)
|
|
||||||
).then(() => Promise.resolve(y))
|
|
||||||
}
|
|
||||||
|
|
||||||
_persistStructsDS (roomName, structsDS) {
|
|
||||||
const encoder = new BinaryEncoder()
|
|
||||||
encoder.writeVarUint(PERSIST_STRUCTS_DS)
|
|
||||||
encoder.writeArrayBuffer(structsDS)
|
|
||||||
return openDB(roomName).then(db => {
|
|
||||||
const t = db.transaction(['updates'], 'readwrite')
|
|
||||||
const updatesStore = t.objectStore('updates')
|
|
||||||
return rtop(updatesStore.put(encoder.createBuffer()))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
_persistStructs (roomName, structs) {
|
|
||||||
const encoder = new BinaryEncoder()
|
|
||||||
encoder.writeVarUint(PERSIST_UPDATE)
|
|
||||||
encoder.writeArrayBuffer(structs)
|
|
||||||
return openDB(roomName).then(db => {
|
|
||||||
const t = db.transaction(['updates'], 'readwrite')
|
|
||||||
const updatesStore = t.objectStore('updates')
|
|
||||||
return rtop(updatesStore.put(encoder.createBuffer()))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
connectY (roomName, y) {
|
|
||||||
if (this._rooms.has(roomName)) {
|
|
||||||
throw new Error('A Y instance is already bound to this room!')
|
|
||||||
}
|
|
||||||
let room = {
|
|
||||||
db: null,
|
|
||||||
dbPromise: null,
|
|
||||||
channel: null,
|
|
||||||
mutex: createMutualExclude(),
|
|
||||||
y
|
|
||||||
}
|
|
||||||
if (typeof BroadcastChannel !== 'undefined') {
|
|
||||||
room.channel = new BroadcastChannel('__yjs__' + roomName)
|
|
||||||
room.channel.addEventListener('message', e => {
|
|
||||||
room.mutex(function () {
|
|
||||||
decodePersisted(y, new BinaryDecoder(e.data))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
y.on('destroyed', () => {
|
|
||||||
this.disconnectY(roomName, y)
|
|
||||||
})
|
|
||||||
y.on('afterTransaction', (y, transaction) => {
|
|
||||||
room.mutex(() => {
|
|
||||||
if (transaction.encodedStructsLen > 0) {
|
|
||||||
const encoder = new BinaryEncoder()
|
|
||||||
const update = new BinaryEncoder()
|
|
||||||
encodeUpdate(y, transaction.encodedStructs, update)
|
|
||||||
const updateBuffer = update.createBuffer()
|
|
||||||
if (room.channel !== null) {
|
|
||||||
room.channel.postMessage(updateBuffer)
|
|
||||||
}
|
|
||||||
if (transaction.encodedStructsLen > 0) {
|
|
||||||
if (room.db !== null) {
|
|
||||||
saveUpdate(room, updateBuffer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
// register document in documentsDB
|
|
||||||
this._documentsDB.then(
|
|
||||||
db =>
|
|
||||||
rtop(db.transaction(['documents'], 'readonly').objectStore('documents').get(roomName))
|
|
||||||
.then(
|
|
||||||
doc => doc === undefined && rtop(db.transaction(['documents'], 'readwrite').objectStore('documents').add({ roomName, serverUpdateCounter: -1 }))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
// open room db and read existing data
|
|
||||||
return room.dbPromise = openDB(roomName)
|
|
||||||
.then(db => {
|
|
||||||
room.db = db
|
|
||||||
const t = room.db.transaction(['updates'], 'readwrite')
|
|
||||||
const updatesStore = t.objectStore('updates')
|
|
||||||
// write current state as update
|
|
||||||
const encoder = new BinaryEncoder()
|
|
||||||
encodeStructsDS(y, encoder)
|
|
||||||
return rtop(updatesStore.put(encoder.createBuffer())).then(() => {
|
|
||||||
// read persisted state
|
|
||||||
return rtop(updatesStore.getAll()).then(updates => {
|
|
||||||
room.mutex(() => {
|
|
||||||
y.transact(() => {
|
|
||||||
updates.forEach(update => {
|
|
||||||
decodePersisted(y, new BinaryDecoder(update))
|
|
||||||
})
|
|
||||||
}, true)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
disconnectY (roomName) {
|
|
||||||
const {
|
|
||||||
db, channel
|
|
||||||
} = this._rooms.get(roomName)
|
|
||||||
db.close()
|
|
||||||
if (channel !== null) {
|
|
||||||
channel.close()
|
|
||||||
}
|
|
||||||
this._rooms.delete(roomName)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove all persisted data that belongs to a room.
|
|
||||||
* Automatically destroys all Yjs all Yjs instances that persist to
|
|
||||||
* the room. If `destroyYjsInstances = false` the persistence functionality
|
|
||||||
* will be removed from the Yjs instances.
|
|
||||||
*/
|
|
||||||
removePersistedData (roomName, destroyYjsInstances = true) {
|
|
||||||
this.disconnectY(roomName)
|
|
||||||
return rtop(indexedDB.deleteDatabase(roomName))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import { integrateRemoteStructs } from '../MessageHandler/integrateRemoteStructs.js'
|
|
||||||
import { writeStructs } from '../MessageHandler/syncStep1.js'
|
|
||||||
import { writeDeleteSet, readDeleteSet } from '../MessageHandler/deleteSet.js'
|
|
||||||
|
|
||||||
export const PERSIST_UPDATE = 0
|
|
||||||
/**
|
|
||||||
* Write an update to an encoder.
|
|
||||||
*
|
|
||||||
* @param {Yjs} y A Yjs instance
|
|
||||||
* @param {BinaryEncoder} updateEncoder I.e. transaction.encodedStructs
|
|
||||||
*/
|
|
||||||
export function encodeUpdate (y, updateEncoder, encoder) {
|
|
||||||
encoder.writeVarUint(PERSIST_UPDATE)
|
|
||||||
encoder.writeBinaryEncoder(updateEncoder)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PERSIST_STRUCTS_DS = 1
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write the current Yjs data model to an encoder.
|
|
||||||
*
|
|
||||||
* @param {Yjs} y A Yjs instance
|
|
||||||
* @param {BinaryEncoder} encoder An encoder to write to
|
|
||||||
*/
|
|
||||||
export function encodeStructsDS (y, encoder) {
|
|
||||||
encoder.writeVarUint(PERSIST_STRUCTS_DS)
|
|
||||||
writeStructs(y, encoder, new Map())
|
|
||||||
writeDeleteSet(y, encoder)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Feed the Yjs instance with the persisted state
|
|
||||||
* @param {Yjs} y A Yjs instance.
|
|
||||||
* @param {BinaryDecoder} decoder A Decoder instance that holds the file content.
|
|
||||||
*/
|
|
||||||
export function decodePersisted (y, decoder) {
|
|
||||||
y.transact(() => {
|
|
||||||
while (decoder.hasContent()) {
|
|
||||||
const contentType = decoder.readVarUint()
|
|
||||||
switch (contentType) {
|
|
||||||
case PERSIST_UPDATE:
|
|
||||||
integrateRemoteStructs(decoder, y)
|
|
||||||
break
|
|
||||||
case PERSIST_STRUCTS_DS:
|
|
||||||
integrateRemoteStructs(decoder, y)
|
|
||||||
readDeleteSet(y, decoder)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, true)
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
|
import Tree from '../Util/Tree.js'
|
||||||
import Tree from '../../lib/Tree.js'
|
import ID from '../Util/ID.js'
|
||||||
import * as ID from '../Util/ID.js'
|
|
||||||
|
|
||||||
class DSNode {
|
class DSNode {
|
||||||
constructor (id, len, gc) {
|
constructor (id, len, gc) {
|
||||||
@@ -30,61 +29,97 @@ export default class DeleteStore extends Tree {
|
|||||||
var n = this.findWithUpperBound(id)
|
var n = this.findWithUpperBound(id)
|
||||||
return n !== null && n._id.user === id.user && id.clock < n._id.clock + n.len
|
return n !== null && n._id.user === id.user && id.clock < n._id.clock + n.len
|
||||||
}
|
}
|
||||||
mark (id, length, gc) {
|
/*
|
||||||
if (length === 0) return
|
* Mark an operation as deleted. returns the deleted node
|
||||||
// Step 1. Unmark range
|
*/
|
||||||
const leftD = this.findWithUpperBound(ID.createID(id.user, id.clock - 1))
|
markDeleted (id, length) {
|
||||||
// Resize left DSNode if necessary
|
if (length == null) {
|
||||||
if (leftD !== null && leftD._id.user === id.user) {
|
throw new Error('length must be defined')
|
||||||
if (leftD._id.clock < id.clock && id.clock < leftD._id.clock + leftD.len) {
|
|
||||||
// node is overlapping. need to resize
|
|
||||||
if (id.clock + length < leftD._id.clock + leftD.len) {
|
|
||||||
// overlaps new mark range and some more
|
|
||||||
// create another DSNode to the right of new mark
|
|
||||||
this.put(new DSNode(ID.createID(id.user, id.clock + length), leftD._id.clock + leftD.len - id.clock - length, leftD.gc))
|
|
||||||
}
|
|
||||||
// resize left DSNode
|
|
||||||
leftD.len = id.clock - leftD._id.clock
|
|
||||||
} // Otherwise there is no overlapping
|
|
||||||
}
|
}
|
||||||
// Resize right DSNode if necessary
|
var n = this.findWithUpperBound(id)
|
||||||
const upper = ID.createID(id.user, id.clock + length - 1)
|
if (n != null && n._id.user === id.user) {
|
||||||
const rightD = this.findWithUpperBound(upper)
|
if (n._id.clock <= id.clock && id.clock <= n._id.clock + n.len) {
|
||||||
if (rightD !== null && rightD._id.user === id.user) {
|
// id is in n's range
|
||||||
if (rightD._id.clock < id.clock + length && id.clock <= rightD._id.clock && id.clock + length < rightD._id.clock + rightD.len) { // we only consider the case where we resize the node
|
var diff = id.clock + length - (n._id.clock + n.len) // overlapping right
|
||||||
const d = id.clock + length - rightD._id.clock
|
if (diff > 0) {
|
||||||
rightD._id = ID.createID(rightD._id.user, rightD._id.clock + d)
|
// id+length overlaps n
|
||||||
rightD.len -= d
|
if (!n.gc) {
|
||||||
|
n.len += diff
|
||||||
|
} else {
|
||||||
|
diff = n._id.clock + n.len - id.clock // overlapping left (id till n.end)
|
||||||
|
if (diff < length) {
|
||||||
|
// a partial deletion
|
||||||
|
let nId = id.clone()
|
||||||
|
nId.clock += diff
|
||||||
|
n = new DSNode(nId, length - diff, false)
|
||||||
|
this.put(n)
|
||||||
|
} else {
|
||||||
|
// already gc'd
|
||||||
|
throw new Error(
|
||||||
|
'DS reached an inconsistent state. Please report this issue!'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// no overlapping, already deleted
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// cannot extend left (there is no left!)
|
||||||
|
n = new DSNode(id, length, false)
|
||||||
|
this.put(n) // TODO: you double-put !!
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// cannot extend left
|
||||||
|
n = new DSNode(id, length, false)
|
||||||
|
this.put(n)
|
||||||
|
}
|
||||||
|
// can extend right?
|
||||||
|
var next = this.findNext(n._id)
|
||||||
|
if (
|
||||||
|
next != null &&
|
||||||
|
n._id.user === next._id.user &&
|
||||||
|
n._id.clock + n.len >= next._id.clock
|
||||||
|
) {
|
||||||
|
diff = n._id.clock + n.len - next._id.clock // from next.start to n.end
|
||||||
|
while (diff >= 0) {
|
||||||
|
// n overlaps with next
|
||||||
|
if (next.gc) {
|
||||||
|
// gc is stronger, so reduce length of n
|
||||||
|
n.len -= diff
|
||||||
|
if (diff >= next.len) {
|
||||||
|
// delete the missing range after next
|
||||||
|
diff = diff - next.len // missing range after next
|
||||||
|
if (diff > 0) {
|
||||||
|
this.put(n) // unneccessary? TODO!
|
||||||
|
this.markDeleted(new ID(next._id.user, next._id.clock + next.len), diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
// we can extend n with next
|
||||||
|
if (diff > next.len) {
|
||||||
|
// n is even longer than next
|
||||||
|
// get next.next, and try to extend it
|
||||||
|
var _next = this.findNext(next._id)
|
||||||
|
this.delete(next._id)
|
||||||
|
if (_next == null || n._id.user !== _next._id.user) {
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
next = _next
|
||||||
|
diff = n._id.clock + n.len - next._id.clock // from next.start to n.end
|
||||||
|
// continue!
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// n just partially overlaps with next. extend n, delete next, and break this loop
|
||||||
|
n.len += next.len - diff
|
||||||
|
this.delete(next._id)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Now we only have to delete all inner marks
|
this.put(n)
|
||||||
const deleteNodeIds = []
|
return n
|
||||||
this.iterate(id, upper, m => {
|
|
||||||
deleteNodeIds.push(m._id)
|
|
||||||
})
|
|
||||||
for (let i = deleteNodeIds.length - 1; i >= 0; i--) {
|
|
||||||
this.delete(deleteNodeIds[i])
|
|
||||||
}
|
|
||||||
let newMark = new DSNode(id, length, gc)
|
|
||||||
// Step 2. Check if we can extend left or right
|
|
||||||
if (leftD !== null && leftD._id.user === id.user && leftD._id.clock + leftD.len === id.clock && leftD.gc === gc) {
|
|
||||||
// We can extend left
|
|
||||||
leftD.len += length
|
|
||||||
newMark = leftD
|
|
||||||
}
|
|
||||||
const rightNext = this.find(ID.createID(id.user, id.clock + length))
|
|
||||||
if (rightNext !== null && rightNext._id.user === id.user && id.clock + length === rightNext._id.clock && gc === rightNext.gc) {
|
|
||||||
// We can merge newMark and rightNext
|
|
||||||
newMark.len += rightNext.len
|
|
||||||
this.delete(rightNext._id)
|
|
||||||
}
|
|
||||||
if (leftD !== newMark) {
|
|
||||||
// only put if we didn't extend left
|
|
||||||
this.put(newMark)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// TODO: exchange markDeleted for mark()
|
|
||||||
markDeleted (id, length) {
|
|
||||||
this.mark(id, length, false)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import Tree from '../../lib/Tree.js'
|
import Tree from '../Util/Tree.js'
|
||||||
import * as ID from '../Util/ID.js'
|
import RootID from '../Util/RootID.js'
|
||||||
import { getStruct } from '../Util/structReferences.js'
|
import { getStruct } from '../Util/structReferences.js'
|
||||||
import { stringifyID, stringifyItemID } from '../message.js'
|
import { logID } from '../MessageHandler/messageToString.js'
|
||||||
import GC from '../Struct/GC.js'
|
|
||||||
|
|
||||||
export default class OperationStore extends Tree {
|
export default class OperationStore extends Tree {
|
||||||
constructor (y) {
|
constructor (y) {
|
||||||
@@ -12,31 +11,23 @@ export default class OperationStore extends Tree {
|
|||||||
logTable () {
|
logTable () {
|
||||||
const items = []
|
const items = []
|
||||||
this.iterate(null, null, function (item) {
|
this.iterate(null, null, function (item) {
|
||||||
if (item.constructor === GC) {
|
items.push({
|
||||||
items.push({
|
id: logID(item),
|
||||||
id: stringifyItemID(item),
|
origin: logID(item._origin === null ? null : item._origin._lastId),
|
||||||
content: item._length,
|
left: logID(item._left === null ? null : item._left._lastId),
|
||||||
deleted: 'GC'
|
right: logID(item._right),
|
||||||
})
|
right_origin: logID(item._right_origin),
|
||||||
} else {
|
parent: logID(item._parent),
|
||||||
items.push({
|
parentSub: item._parentSub,
|
||||||
id: stringifyItemID(item),
|
deleted: item._deleted,
|
||||||
origin: item._origin === null ? '()' : stringifyID(item._origin._lastId),
|
content: JSON.stringify(item._content)
|
||||||
left: item._left === null ? '()' : stringifyID(item._left._lastId),
|
})
|
||||||
right: stringifyItemID(item._right),
|
|
||||||
right_origin: stringifyItemID(item._right_origin),
|
|
||||||
parent: stringifyItemID(item._parent),
|
|
||||||
parentSub: item._parentSub,
|
|
||||||
deleted: item._deleted,
|
|
||||||
content: JSON.stringify(item._content)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
console.table(items)
|
console.table(items)
|
||||||
}
|
}
|
||||||
get (id) {
|
get (id) {
|
||||||
let struct = this.find(id)
|
let struct = this.find(id)
|
||||||
if (struct === null && id instanceof ID.RootID) {
|
if (struct === null && id instanceof RootID) {
|
||||||
const Constr = getStruct(id.type)
|
const Constr = getStruct(id.type)
|
||||||
const y = this.y
|
const y = this.y
|
||||||
struct = new Constr()
|
struct = new Constr()
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
import * as ID from '../Util/ID.js'
|
import ID from '../Util/ID.js'
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {Map<number, number>} StateSet
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default class StateStore {
|
export default class StateStore {
|
||||||
constructor (y) {
|
constructor (y) {
|
||||||
@@ -22,14 +18,14 @@ export default class StateStore {
|
|||||||
const user = this.y.userID
|
const user = this.y.userID
|
||||||
const state = this.getState(user)
|
const state = this.getState(user)
|
||||||
this.setState(user, state + len)
|
this.setState(user, state + len)
|
||||||
return ID.createID(user, state)
|
return new ID(user, state)
|
||||||
}
|
}
|
||||||
updateRemoteState (struct) {
|
updateRemoteState (struct) {
|
||||||
let user = struct._id.user
|
let user = struct._id.user
|
||||||
let userState = this.state.get(user)
|
let userState = this.state.get(user)
|
||||||
while (struct !== null && struct._id.clock === userState) {
|
while (struct !== null && struct._id.clock === userState) {
|
||||||
userState += struct._length
|
userState += struct._length
|
||||||
struct = this.y.os.get(ID.createID(user, userState))
|
struct = this.y.os.get(new ID(user, userState))
|
||||||
}
|
}
|
||||||
this.state.set(user, userState)
|
this.state.set(user, userState)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,29 @@
|
|||||||
import { getStructReference } from '../Util/structReferences.js'
|
import { getReference } from '../Util/structReferences.js'
|
||||||
import * as ID from '../Util/ID.js'
|
import ID from '../Util/ID.js'
|
||||||
import { stringifyID } from '../message.js'
|
import { logID } from '../MessageHandler/messageToString.js'
|
||||||
import { writeStructToTransaction } from '../Util/Transaction.js'
|
|
||||||
import * as decoding from '../../lib/decoding.js'
|
|
||||||
import * as encoding from '../../lib/encoding.js'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* Delete all items in an ID-range
|
||||||
* Delete all items in an ID-range.
|
* TODO: implement getItemCleanStartNode for better performance (only one lookup)
|
||||||
* Does not create delete operations!
|
|
||||||
* TODO: implement getItemCleanStartNode for better performance (only one lookup).
|
|
||||||
*/
|
*/
|
||||||
export function deleteItemRange (y, user, clock, range, gcChildren) {
|
export function deleteItemRange (y, user, clock, range) {
|
||||||
let item = y.os.getItemCleanStart(ID.createID(user, clock))
|
const createDelete = y.connector._forwardAppliedStructs
|
||||||
|
let item = y.os.getItemCleanStart(new ID(user, clock))
|
||||||
if (item !== null) {
|
if (item !== null) {
|
||||||
if (!item._deleted) {
|
if (!item._deleted) {
|
||||||
item._splitAt(y, range)
|
item._splitAt(y, range)
|
||||||
item._delete(y, false, true)
|
item._delete(y, createDelete)
|
||||||
}
|
}
|
||||||
let itemLen = item._length
|
let itemLen = item._length
|
||||||
range -= itemLen
|
range -= itemLen
|
||||||
clock += itemLen
|
clock += itemLen
|
||||||
if (range > 0) {
|
if (range > 0) {
|
||||||
let node = y.os.findNode(ID.createID(user, clock))
|
let node = y.os.findNode(new ID(user, clock))
|
||||||
while (node !== null && node.val !== null && range > 0 && node.val._id.equals(ID.createID(user, clock))) {
|
while (node !== null && range > 0 && node.val._id.equals(new ID(user, clock))) {
|
||||||
const nodeVal = node.val
|
const nodeVal = node.val
|
||||||
if (!nodeVal._deleted) {
|
if (!nodeVal._deleted) {
|
||||||
nodeVal._splitAt(y, range)
|
nodeVal._splitAt(y, range)
|
||||||
nodeVal._delete(y, false, gcChildren)
|
nodeVal._delete(y, createDelete)
|
||||||
}
|
}
|
||||||
const nodeLen = nodeVal._length
|
const nodeLen = nodeVal._length
|
||||||
range -= nodeLen
|
range -= nodeLen
|
||||||
@@ -39,92 +35,50 @@ export function deleteItemRange (y, user, clock, range, gcChildren) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* Delete is not a real struct. It will not be saved in OS
|
||||||
* A Delete change is not a real Item, but it provides the same interface as an
|
|
||||||
* Item. The only difference is that it will not be saved in the ItemStore
|
|
||||||
* (OperationStore), but instead it is safed in the DeleteStore.
|
|
||||||
*/
|
*/
|
||||||
export default class Delete {
|
export default class Delete {
|
||||||
constructor () {
|
constructor () {
|
||||||
/**
|
|
||||||
* @type {ID.ID}
|
|
||||||
*/
|
|
||||||
this._targetID = null
|
|
||||||
/**
|
|
||||||
* @type {import('./Item.js').default}
|
|
||||||
*/
|
|
||||||
this._target = null
|
this._target = null
|
||||||
this._length = null
|
this._length = null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
* Read the next Item in a Decoder and fill this Item with the read data.
|
|
||||||
*
|
|
||||||
* This is called when data is received from a remote peer.
|
|
||||||
*
|
|
||||||
* @param {import('../Y.js').default} y The Yjs instance that this Item belongs to.
|
|
||||||
* @param {decoding.Decoder} decoder The decoder object to read data from.
|
|
||||||
*/
|
|
||||||
_fromBinary (y, decoder) {
|
_fromBinary (y, decoder) {
|
||||||
// TODO: set target, and add it to missing if not found
|
// TODO: set target, and add it to missing if not found
|
||||||
// There is an edge case in p2p networks!
|
// There is an edge case in p2p networks!
|
||||||
/**
|
const targetID = decoder.readID()
|
||||||
* @type {any}
|
|
||||||
*/
|
|
||||||
const targetID = ID.decode(decoder)
|
|
||||||
this._targetID = targetID
|
this._targetID = targetID
|
||||||
this._length = decoding.readVarUint(decoder)
|
this._length = decoder.readVarUint()
|
||||||
if (y.os.getItem(targetID) === null) {
|
if (y.os.getItem(targetID) === null) {
|
||||||
return [targetID]
|
return [targetID]
|
||||||
} else {
|
} else {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
* Transform the properties of this type to binary and write it to an
|
|
||||||
* BinaryEncoder.
|
|
||||||
*
|
|
||||||
* This is called when this Item is sent to a remote peer.
|
|
||||||
*
|
|
||||||
* @param {encoding.Encoder} encoder The encoder to write data to.
|
|
||||||
*/
|
|
||||||
_toBinary (encoder) {
|
_toBinary (encoder) {
|
||||||
encoding.writeUint8(encoder, getStructReference(this.constructor))
|
encoder.writeUint8(getReference(this.constructor))
|
||||||
this._targetID.encode(encoder)
|
encoder.writeID(this._targetID)
|
||||||
encoding.writeVarUint(encoder, this._length)
|
encoder.writeVarUint(this._length)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* - If created remotely (a remote user deleted something),
|
||||||
* Integrates this Item into the shared structure.
|
|
||||||
*
|
|
||||||
* This method actually applies the change to the Yjs instance. In the case of
|
|
||||||
* Delete it marks the delete target as deleted.
|
|
||||||
*
|
|
||||||
* * If created remotely (a remote user deleted something),
|
|
||||||
* this Delete is applied to all structs in id-range.
|
* this Delete is applied to all structs in id-range.
|
||||||
* * If created lokally (e.g. when y-array deletes a range of elements),
|
* - If created lokally (e.g. when y-array deletes a range of elements),
|
||||||
* this struct is broadcasted only (it is already executed)
|
* this struct is broadcasted only (it is already executed)
|
||||||
*/
|
*/
|
||||||
_integrate (y, locallyCreated = false) {
|
_integrate (y, locallyCreated = false) {
|
||||||
if (!locallyCreated) {
|
if (!locallyCreated) {
|
||||||
// from remote
|
// from remote
|
||||||
const id = this._targetID
|
const id = this._targetID
|
||||||
deleteItemRange(y, id.user, id.clock, this._length, false)
|
deleteItemRange(y, id.user, id.clock, this._length)
|
||||||
|
} else {
|
||||||
|
// from local
|
||||||
|
y.connector.broadcastStruct(this)
|
||||||
|
}
|
||||||
|
if (y.persistence !== null) {
|
||||||
|
y.persistence.saveOperations(this)
|
||||||
}
|
}
|
||||||
writeStructToTransaction(y._transaction, this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Transform this YXml Type to a readable format.
|
|
||||||
* Useful for logging as all Items and Delete implement this method.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_logString () {
|
_logString () {
|
||||||
return `Delete - target: ${stringifyID(this._targetID)}, len: ${this._length}`
|
return `Delete - target: ${logID(this._targetID)}, len: ${this._length}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user