Compare commits
19 Commits
experiment
...
ydb-integr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67bbc0a3fe | ||
|
|
e1ece6dc66 | ||
|
|
fe038822a3 | ||
|
|
dece14486c | ||
|
|
2daffbc2ca | ||
|
|
4c01a34d09 | ||
|
|
3b08267daa | ||
|
|
b98ebddb69 | ||
|
|
9d5bf50676 | ||
|
|
c0972f8158 | ||
|
|
548125a944 | ||
|
|
a7b124ca6e | ||
|
|
4022374620 | ||
|
|
860e4d7af6 | ||
|
|
6376d69b58 | ||
|
|
5cf6f45f19 | ||
|
|
967903673b | ||
|
|
db5312443e | ||
|
|
dbda07424b |
12
.babelrc
12
.babelrc
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"presets": [
|
||||
["latest", {
|
||||
"es2015": {
|
||||
"modules": false
|
||||
}
|
||||
}]
|
||||
],
|
||||
"plugins": [
|
||||
"external-helpers"
|
||||
]
|
||||
}
|
||||
14
.flowconfig
14
.flowconfig
@@ -1,14 +0,0 @@
|
||||
[ignore]
|
||||
.*/node_modules/.*
|
||||
.*/dist/.*
|
||||
.*/build/.*
|
||||
|
||||
[include]
|
||||
./src/
|
||||
./tests-lib/
|
||||
./test/
|
||||
|
||||
[libs]
|
||||
./declarations/
|
||||
|
||||
[options]
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,3 +5,4 @@ docs
|
||||
/examples/yjs-dist.js*
|
||||
.vscode
|
||||
.yjsPersisted
|
||||
build
|
||||
@@ -248,7 +248,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.connector.disconnect()
|
||||
* Force to disconnect this instance from the other instances
|
||||
* y.connector.reconnect()
|
||||
* y.connector.connect()
|
||||
* Try to reconnect to the other instances (needs to be supported by the
|
||||
connector)
|
||||
* Not supported by y-xmpp
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
</head>
|
||||
<script src="./index.mjs" type="module"></script>
|
||||
<script src="./index.js" type="module"></script>
|
||||
</head>
|
||||
<body>
|
||||
<label for="room">Room: </label>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
|
||||
import YWebsocketsConnector from '../../src/Connectors/WebsocketsConnector/WebsocketsConnector.mjs'
|
||||
import Y from '../../src/Y.mjs'
|
||||
import DomBinding from '../../src/Bindings/DomBinding/DomBinding.mjs'
|
||||
import UndoManager from '../../src/Util/UndoManager.mjs'
|
||||
import YXmlFragment from '../../src/Types/YXml/YXmlFragment.mjs'
|
||||
import YXmlText from '../../src/Types/YXml/YXmlText.mjs'
|
||||
import YXmlElement from '../../src/Types/YXml/YXmlElement.mjs'
|
||||
import YIndexdDBPersistence from '../../src/Persistences/IndexedDBPersistence.mjs'
|
||||
import YWebsocketsConnector from '../../src/Connectors/WebsocketsConnector/WebsocketsConnector.js'
|
||||
import Y from '../../src/Y.js'
|
||||
import DomBinding from '../../src/Bindings/DomBinding/DomBinding.js'
|
||||
import UndoManager from '../../src/Util/UndoManager.js'
|
||||
import YXmlFragment from '../../src/Types/YXml/YXmlFragment.js'
|
||||
import YXmlText from '../../src/Types/YXml/YXmlText.js'
|
||||
import YXmlElement from '../../src/Types/YXml/YXmlElement.js'
|
||||
import YIndexdDBPersistence from '../../src/Persistences/IndexedDBPersistence.js'
|
||||
|
||||
const connector = new YWebsocketsConnector()
|
||||
const persistence = new YIndexdDBPersistence()
|
||||
@@ -1,8 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
</head>
|
||||
<script src="./index.mjs" type="module"></script>
|
||||
<script src="./index.js" type="module"></script>
|
||||
<link rel="stylesheet" type="text/css" href="style.css">
|
||||
</head>
|
||||
<body contenteditable="true">
|
||||
<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>
|
||||
</html>
|
||||
132
examples/notes/index.js
Normal file
132
examples/notes/index.js
Normal file
@@ -0,0 +1,132 @@
|
||||
/* 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,48 +0,0 @@
|
||||
|
||||
import IndexedDBPersistence from '../../src/Persistences/IndexeddbPersistence.mjs'
|
||||
import YWebsocketsConnector from '../../src/Connectors/WebsocketsConnector/WebsocketsConnector.mjs'
|
||||
import Y from '../../src/Y.mjs'
|
||||
import YXmlFragment from '../../src/Types/YXml/YXmlFragment.mjs'
|
||||
|
||||
const yCollection = new YCollection(new YWebsocketsConnector(), new IndexedDBPersistence())
|
||||
|
||||
const y = yCollection.getDocument('my-notes')
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
persistence.addConnector(persistence)
|
||||
|
||||
const y = new Y()
|
||||
await persistence.persistY(y)
|
||||
|
||||
|
||||
connector.connectY('html-editor', y)
|
||||
persistence.connectY('html-editor', y)
|
||||
|
||||
|
||||
|
||||
|
||||
window.connector = connector
|
||||
|
||||
window.onload = function () {
|
||||
window.domBinding = new DomBinding(window.yXmlType, document.body, { scrollingElement: document.scrollingElement })
|
||||
}
|
||||
|
||||
window.y = y
|
||||
window.yXmlType = y.define('xml', YXmlFragment)
|
||||
window.undoManager = new UndoManager(window.yXmlType, {
|
||||
captureTimeout: 500
|
||||
})
|
||||
|
||||
document.onkeydown = function interceptUndoRedo (e) {
|
||||
if (e.keyCode === 90 && (e.metaKey || e.ctrlKey)) {
|
||||
if (!e.shiftKey) {
|
||||
window.undoManager.undo()
|
||||
} else {
|
||||
window.undoManager.redo()
|
||||
}
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
100
examples/notes/style.css
Normal file
100
examples/notes/style.css
Normal file
@@ -0,0 +1,100 @@
|
||||
.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;
|
||||
}
|
||||
@@ -20,7 +20,7 @@ let quill = new Quill('#quill-container', {
|
||||
[{ header: [1, 2, false] }],
|
||||
['bold', 'italic', 'underline'],
|
||||
['image', 'code-block'],
|
||||
[{ color: [] }, { background: [] }], // Snow theme fills in values
|
||||
[{ color: [] }, { background: [] }], // Snow theme fills in values
|
||||
[{ script: 'sub' }, { script: 'super' }],
|
||||
['link', 'image'],
|
||||
['link', 'code-block'],
|
||||
@@ -31,7 +31,7 @@ let quill = new Quill('#quill-container', {
|
||||
}
|
||||
},
|
||||
placeholder: 'Compose an epic...',
|
||||
theme: 'snow' // or 'bubble'
|
||||
theme: 'snow' // or 'bubble'
|
||||
})
|
||||
|
||||
let cursors = quill.getModule('cursors')
|
||||
|
||||
@@ -13,7 +13,7 @@ let quill = new Quill('#quill-container', {
|
||||
[{ header: [1, 2, false] }],
|
||||
['bold', 'italic', 'underline'],
|
||||
['image', 'code-block'],
|
||||
[{ color: [] }, { background: [] }], // Snow theme fills in values
|
||||
[{ color: [] }, { background: [] }], // Snow theme fills in values
|
||||
[{ script: 'sub' }, { script: 'super' }],
|
||||
['link', 'image'],
|
||||
['link', 'code-block'],
|
||||
@@ -21,7 +21,7 @@ let quill = new Quill('#quill-container', {
|
||||
]
|
||||
},
|
||||
placeholder: 'Compose an epic...',
|
||||
theme: 'snow' // or 'bubble'
|
||||
theme: 'snow' // or 'bubble'
|
||||
})
|
||||
|
||||
let yText = y.define('quill', Y.Text)
|
||||
|
||||
@@ -4,7 +4,7 @@ import commonjs from 'rollup-plugin-commonjs'
|
||||
var pkg = require('./package.json')
|
||||
|
||||
export default {
|
||||
input: 'yjs-dist.mjs',
|
||||
input: 'yjs-dist.js',
|
||||
name: 'Y',
|
||||
output: {
|
||||
file: 'yjs-dist.js',
|
||||
|
||||
@@ -35,7 +35,7 @@ Y({
|
||||
toolbar: [
|
||||
[{ size: ['small', false, 'large', 'huge'] }],
|
||||
['bold', 'italic', 'underline'],
|
||||
[{ color: [] }, { background: [] }], // Snow theme fills in values
|
||||
[{ color: [] }, { background: [] }], // Snow theme fills in values
|
||||
[{ script: 'sub' }, { script: 'super' }],
|
||||
['link', 'image'],
|
||||
['link', 'code-block'],
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
<html>
|
||||
<body>
|
||||
<textarea style="width:80%;" rows=40 autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
|
||||
<script src="../../y.js"></script>
|
||||
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
|
||||
<script src="./index.js"></script>
|
||||
<script type="module" src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
/* global Y */
|
||||
/* eslint-env browser */
|
||||
import * as Y from '../../src/index.js'
|
||||
import WebsocketProvider from '../../provider/websocket/WebSocketProvider.js'
|
||||
|
||||
let y = new Y('textarea-example', {
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
url: 'http://127.0.0.1:1234'
|
||||
}
|
||||
})
|
||||
const provider = new WebsocketProvider('ws://localhost:1234/')
|
||||
const ydocument = provider.get('textarea')
|
||||
const type = ydocument.define('textarea', Y.Text)
|
||||
const textarea = document.querySelector('textarea')
|
||||
const binding = new Y.TextareaBinding(type, textarea)
|
||||
|
||||
window.yTextarea = y
|
||||
|
||||
// bind the textarea to a shared text element
|
||||
let type = y.define('textarea', Y.Text)
|
||||
let textarea = document.querySelector('textarea')
|
||||
window.binding = new Y.TextareaBinding(type, textarea)
|
||||
window.textareaExample = {
|
||||
provider, ydocument, type, textarea, binding
|
||||
}
|
||||
|
||||
7
lib/binary.js
Normal file
7
lib/binary.js
Normal file
@@ -0,0 +1,7 @@
|
||||
|
||||
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
Normal file
168
lib/decoding.js
Normal file
@@ -0,0 +1,168 @@
|
||||
|
||||
/* 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
Normal file
218
lib/encoding.js
Normal file
@@ -0,0 +1,218 @@
|
||||
|
||||
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)
|
||||
}
|
||||
49
lib/encoding.test.js
Normal file
49
lib/encoding.test.js
Normal file
@@ -0,0 +1,49 @@
|
||||
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!')
|
||||
}
|
||||
63
lib/globals.js
Normal file
63
lib/globals.js
Normal file
@@ -0,0 +1,63 @@
|
||||
/* 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
Normal file
159
lib/idb.js
Normal file
@@ -0,0 +1,159 @@
|
||||
/* 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)
|
||||
34
lib/idb.test.js
Normal file
34
lib/idb.test.js
Normal file
@@ -0,0 +1,34 @@
|
||||
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')
|
||||
}
|
||||
})
|
||||
})
|
||||
23
lib/logging.js
Normal file
23
lib/logging.js
Normal file
@@ -0,0 +1,23 @@
|
||||
|
||||
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)))
|
||||
2
lib/math.js
Normal file
2
lib/math.js
Normal file
@@ -0,0 +1,2 @@
|
||||
|
||||
export const floor = Math.floor
|
||||
@@ -1,32 +1,30 @@
|
||||
// TODO: rename mutex
|
||||
|
||||
/**
|
||||
* Creates a mutual exclude function with the following property:
|
||||
*
|
||||
* @example
|
||||
* const mutualExclude = createMutualExclude()
|
||||
* mutualExclude(function () {
|
||||
* const mutex = createMutex()
|
||||
* mutex(function () {
|
||||
* // This function is immediately executed
|
||||
* mutualExclude(function () {
|
||||
* mutex(function () {
|
||||
* // This function is never executed, as it is called with the same
|
||||
* // mutualExclude
|
||||
* // mutex function
|
||||
* })
|
||||
* })
|
||||
*
|
||||
* @return {Function} A mutual exclude function
|
||||
* @public
|
||||
*/
|
||||
export function createMutualExclude () {
|
||||
var token = true
|
||||
return function mutualExclude (f, g) {
|
||||
export const createMutex = () => {
|
||||
let token = true
|
||||
return (f, g) => {
|
||||
if (token) {
|
||||
token = false
|
||||
try {
|
||||
f()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
token = true
|
||||
}
|
||||
token = true
|
||||
} else if (g !== undefined) {
|
||||
g()
|
||||
}
|
||||
2
lib/number.js
Normal file
2
lib/number.js
Normal file
@@ -0,0 +1,2 @@
|
||||
export const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER
|
||||
export const MIN_SAFE_INTEGER = Number.MIN_SAFE_INTEGER
|
||||
66
lib/random/PRNG/Mt19937.js
Normal file
66
lib/random/PRNG/Mt19937.js
Normal file
@@ -0,0 +1,66 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
48
lib/random/PRNG/PRNG.tests.js
Normal file
48
lib/random/PRNG/PRNG.tests.js
Normal file
@@ -0,0 +1,48 @@
|
||||
|
||||
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)
|
||||
5
lib/random/PRNG/README.md
Normal file
5
lib/random/PRNG/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# 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
|
||||
98
lib/random/PRNG/Xoroshiro128plus.js
Normal file
98
lib/random/PRNG/Xoroshiro128plus.js
Normal file
@@ -0,0 +1,98 @@
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
*/
|
||||
26
lib/random/PRNG/Xorshift32.js
Normal file
26
lib/random/PRNG/Xorshift32.js
Normal file
@@ -0,0 +1,26 @@
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
}
|
||||
131
lib/random/random.js
Normal file
131
lib/random/random.js
Normal file
@@ -0,0 +1,131 @@
|
||||
|
||||
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)]
|
||||
110
lib/random/random.test.js
Normal file
110
lib/random/random.test.js
Normal file
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
*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.')
|
||||
})
|
||||
}
|
||||
*/
|
||||
@@ -12,7 +12,7 @@
|
||||
*
|
||||
* @typedef {Object} SimpleDiff
|
||||
* @property {Number} pos The index where changes were applied
|
||||
* @property {Number} delete The number of characters to delete starting
|
||||
* @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`
|
||||
2
lib/string.js
Normal file
2
lib/string.js
Normal file
@@ -0,0 +1,2 @@
|
||||
export const fromCharCode = String.fromCharCode
|
||||
export const fromCodePoint = String.fromCodePoint
|
||||
33
lib/test.js
Normal file
33
lib/test.js
Normal file
@@ -0,0 +1,33 @@
|
||||
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')
|
||||
}
|
||||
}
|
||||
3
lib/time.js
Normal file
3
lib/time.js
Normal file
@@ -0,0 +1,3 @@
|
||||
|
||||
export const getDate = () => new Date()
|
||||
export const getUnixTime = () => getDate().getTime()
|
||||
4103
package-lock.json
generated
4103
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
43
package.json
43
package.json
@@ -1,27 +1,19 @@
|
||||
{
|
||||
"name": "yjs",
|
||||
"version": "13.0.0-60",
|
||||
"version": "13.0.0-66",
|
||||
"description": "A framework for real-time p2p shared editing on any data",
|
||||
"main": "./y.node.js",
|
||||
"browser": "./y.js",
|
||||
"module": "./src/y.js",
|
||||
"module": "./src/index.js",
|
||||
"scripts": {
|
||||
"start": "node --experimental-modules src/Connectors/WebsocketsConnector/server.mjs",
|
||||
"test": "npm run lint",
|
||||
"debug": "concurrently 'rollup -wc rollup.test.js' 'cutest-serve y.test.js -o'",
|
||||
"lint": "standard src/**/*.mjs test/**/*.mjs tests-lib/**/*.mjs",
|
||||
"lint": "standard src/**/*.js test/**/*.js tests-lib/**/*.js",
|
||||
"docs": "esdoc",
|
||||
"serve-docs": "npm run docs && serve ./docs/",
|
||||
"dist": "rollup -c rollup.browser.js; rollup -c rollup.node.js",
|
||||
"watch": "concurrently 'rollup -wc rollup.browser.js' 'rollup -wc rollup.node.js'",
|
||||
"postversion": "npm run dist",
|
||||
"postpublish": "tag-dist-files --overwrite-existing-tag",
|
||||
"demos": "concurrently 'node --experimental-modules src/Connectors/WebsocketsConnector/server.mjs' 'http-server'"
|
||||
},
|
||||
"now": {
|
||||
"engines": {
|
||||
"node": "10.x.x"
|
||||
}
|
||||
"postversion": "npm run dist"
|
||||
},
|
||||
"files": [
|
||||
"y.*",
|
||||
@@ -56,32 +48,29 @@
|
||||
},
|
||||
"homepage": "http://y-js.org",
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.24.1",
|
||||
"babel-cli": "^6.26.0",
|
||||
"babel-plugin-external-helpers": "^6.22.0",
|
||||
"babel-plugin-transform-regenerator": "^6.24.1",
|
||||
"babel-plugin-transform-regenerator": "^6.26.0",
|
||||
"babel-plugin-transform-runtime": "^6.23.0",
|
||||
"babel-preset-latest": "^6.24.1",
|
||||
"chance": "^1.0.9",
|
||||
"codemirror": "^5.37.0",
|
||||
"concurrently": "^3.4.0",
|
||||
"concurrently": "^3.6.1",
|
||||
"cutest": "^0.1.9",
|
||||
"esdoc": "^1.0.4",
|
||||
"esdoc": "^1.1.0",
|
||||
"esdoc-standard-plugin": "^1.0.0",
|
||||
"quill": "^1.3.5",
|
||||
"quill-cursors": "^1.0.2",
|
||||
"quill": "^1.3.6",
|
||||
"quill-cursors": "^1.0.3",
|
||||
"rollup": "^0.58.2",
|
||||
"rollup-plugin-babel": "^2.7.1",
|
||||
"rollup-plugin-commonjs": "^8.0.2",
|
||||
"rollup-plugin-inject": "^2.0.0",
|
||||
"rollup-plugin-multi-entry": "^2.0.1",
|
||||
"rollup-plugin-node-resolve": "^3.0.0",
|
||||
"rollup-plugin-commonjs": "^8.4.1",
|
||||
"rollup-plugin-inject": "^2.2.0",
|
||||
"rollup-plugin-multi-entry": "^2.0.2",
|
||||
"rollup-plugin-node-resolve": "^3.4.0",
|
||||
"rollup-plugin-uglify": "^1.0.2",
|
||||
"rollup-regenerator-runtime": "^6.23.1",
|
||||
"rollup-watch": "^3.2.2",
|
||||
"standard": "^11.0.1",
|
||||
"tag-dist-files": "^0.1.6"
|
||||
"standard": "^11.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"uws": "^10.148.0"
|
||||
"ws": "^6.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
85
provider/websocket/WebSocketProvider.js
Normal file
85
provider/websocket/WebSocketProvider.js
Normal file
@@ -0,0 +1,85 @@
|
||||
/* 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
|
||||
}
|
||||
}
|
||||
53
provider/websocket/server.js
Normal file
53
provider/websocket/server.js
Normal file
@@ -0,0 +1,53 @@
|
||||
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,7 +5,7 @@ import commonjs from 'rollup-plugin-commonjs'
|
||||
var pkg = require('./package.json')
|
||||
|
||||
export default {
|
||||
input: 'src/Y.dist.mjs',
|
||||
input: 'src/Y.dist.js',
|
||||
name: 'Y',
|
||||
sourcemap: true,
|
||||
output: {
|
||||
|
||||
@@ -1,28 +1,18 @@
|
||||
import nodeResolve from 'rollup-plugin-node-resolve'
|
||||
import commonjs from 'rollup-plugin-commonjs'
|
||||
var pkg = require('./package.json')
|
||||
const pkg = require('./package.json')
|
||||
|
||||
export default {
|
||||
input: 'src/Y.dist.mjs',
|
||||
nameame: 'Y',
|
||||
sourcemap: true,
|
||||
input: 'src/index.js',
|
||||
output: {
|
||||
file: 'y.node.js',
|
||||
format: 'cjs'
|
||||
},
|
||||
plugins: [
|
||||
nodeResolve({
|
||||
main: true,
|
||||
module: true,
|
||||
browser: true
|
||||
}),
|
||||
commonjs()
|
||||
],
|
||||
banner: `
|
||||
name: 'Y',
|
||||
file: 'build/node/index.js',
|
||||
format: 'cjs',
|
||||
sourcemap: true,
|
||||
banner: `
|
||||
/**
|
||||
* ${pkg.name} - ${pkg.description}
|
||||
* @version v${pkg.version}
|
||||
* @license ${pkg.license}
|
||||
*/
|
||||
`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import commonjs from 'rollup-plugin-commonjs'
|
||||
import multiEntry from 'rollup-plugin-multi-entry'
|
||||
|
||||
export default {
|
||||
input: 'test/index.mjs',
|
||||
input: 'test/index.js',
|
||||
name: 'y-tests',
|
||||
sourcemap: true,
|
||||
output: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
import { createMutualExclude } from '../Util/mutualExclude.mjs'
|
||||
import { createMutex } from '../../lib/mutex.js'
|
||||
|
||||
/**
|
||||
* Abstract class for bindings.
|
||||
@@ -35,7 +35,7 @@ export default class Binding {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
this._mutualExclude = createMutualExclude()
|
||||
this._mutualExclude = createMutex()
|
||||
}
|
||||
/**
|
||||
* Remove all data observers (both from the type and the target).
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
import Binding from '../Binding.mjs'
|
||||
import simpleDiff from '../../Util/simpleDiff.mjs'
|
||||
import { getRelativePosition, fromRelativePosition } from '../../Util/relativePosition.mjs'
|
||||
import Binding from '../Binding.js'
|
||||
import simpleDiff from '../../Util/simpleDiff.js'
|
||||
import { getRelativePosition, fromRelativePosition } from '../../Util/relativePosition.js'
|
||||
|
||||
function typeObserver () {
|
||||
this._mutualExclude(() => {
|
||||
@@ -1,11 +1,16 @@
|
||||
/* global MutationObserver */
|
||||
/* global MutationObserver, getSelection */
|
||||
|
||||
import Binding from '../Binding.mjs'
|
||||
import { createAssociation, removeAssociation } from './util.mjs'
|
||||
import { beforeTransactionSelectionFixer, afterTransactionSelectionFixer } from './selection.mjs'
|
||||
import { defaultFilter, applyFilterOnType } from './filter.mjs'
|
||||
import typeObserver from './typeObserver.mjs'
|
||||
import domObserver from './domObserver.mjs'
|
||||
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.
|
||||
@@ -25,7 +30,7 @@ export default class DomBinding extends Binding {
|
||||
* @param {Element} target The bind target. Mirrors the target.
|
||||
* @param {Object} [opts] Optional configurations
|
||||
|
||||
* @param {FilterFunction} [opts.filter=defaultFilter] The filter function to use.
|
||||
* @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
|
||||
@@ -47,7 +52,7 @@ export default class DomBinding extends Binding {
|
||||
/**
|
||||
* Defines which DOM attributes and elements to filter out.
|
||||
* Also filters remote changes.
|
||||
* @type {FilterFunction}
|
||||
* @type {DomFilter}
|
||||
*/
|
||||
this.filter = opts.filter || defaultFilter
|
||||
// set initial value
|
||||
@@ -56,7 +61,7 @@ export default class DomBinding extends Binding {
|
||||
target.insertBefore(child.toDom(opts.document, opts.hooks, this), null)
|
||||
})
|
||||
this._typeObserver = typeObserver.bind(this)
|
||||
this._domObserver = (mutations) => {
|
||||
this._domObserver = mutations => {
|
||||
domObserver.call(this, mutations, opts.document)
|
||||
}
|
||||
type.observeDeep(this._typeObserver)
|
||||
@@ -67,16 +72,26 @@ export default class DomBinding extends Binding {
|
||||
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())
|
||||
beforeTransactionSelectionFixer(y, this, transaction, remote)
|
||||
this._mutualExclude(() => {
|
||||
beforeTransactionSelectionFixer(this, remote)
|
||||
})
|
||||
}
|
||||
y.on('beforeTransaction', this._beforeTransactionHandler)
|
||||
this._afterTransactionHandler = (y, transaction, remote) => {
|
||||
afterTransactionSelectionFixer(y, this, 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
|
||||
@@ -108,13 +123,69 @@ export default class DomBinding extends Binding {
|
||||
|
||||
/**
|
||||
* NOTE: currently does not apply filter to existing elements!
|
||||
* @param {FilterFunction} filter The filter function to use from now on.
|
||||
* @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.
|
||||
*/
|
||||
@@ -127,14 +198,14 @@ export default class DomBinding extends Binding {
|
||||
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
|
||||
*/
|
||||
/**
|
||||
* 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,11 +1,11 @@
|
||||
|
||||
import YXmlHook from '../../Types/YXml/YXmlHook.mjs'
|
||||
import YXmlHook from '../../Types/YXml/YXmlHook.js'
|
||||
import {
|
||||
iterateUntilUndeleted,
|
||||
removeAssociation,
|
||||
insertNodeHelper } from './util.mjs'
|
||||
import diff from '../../Util/simpleDiff.mjs'
|
||||
import YXmlFragment from '../../Types/YXml/YXmlFragment.mjs'
|
||||
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
|
||||
66
src/Bindings/DomBinding/domToType.js
Normal file
66
src/Bindings/DomBinding/domToType.js
Normal file
@@ -0,0 +1,66 @@
|
||||
/* 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,61 +0,0 @@
|
||||
|
||||
import YXmlText from '../../Types/YXml/YXmlText.mjs'
|
||||
import YXmlHook from '../../Types/YXml/YXmlHook.mjs'
|
||||
import YXmlElement from '../../Types/YXml/YXmlElement.mjs'
|
||||
import { createAssociation, domsToTypes } from './util.mjs'
|
||||
import { filterDomAttributes, defaultFilter } from './filter.mjs'
|
||||
|
||||
/**
|
||||
* Creates a Yjs type (YXml) based on the contents of a DOM Element.
|
||||
*
|
||||
* @param {Element|TextNode} element The DOM Element
|
||||
* @param {?Document} _document Optional. Provide the global document object
|
||||
* @param {Hooks} [hooks = {}] Optional. Set of Yjs Hooks
|
||||
* @param {Filter} [filter=defaultFilter] Optional. Dom element filter
|
||||
* @param {?DomBinding} binding Warning: This property is for internal use only!
|
||||
* @return {YXmlElement | YXmlText}
|
||||
*/
|
||||
export default function domToType (element, _document = document, hooks = {}, filter = defaultFilter, binding) {
|
||||
let type
|
||||
switch (element.nodeType) {
|
||||
case _document.ELEMENT_NODE:
|
||||
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.`)
|
||||
delete 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)
|
||||
}
|
||||
break
|
||||
case _document.TEXT_NODE:
|
||||
type = new YXmlText()
|
||||
type.insert(0, element.nodeValue)
|
||||
break
|
||||
default:
|
||||
throw new Error('Can\'t transform this node type to a YXml type!')
|
||||
}
|
||||
createAssociation(binding, element, type)
|
||||
return type
|
||||
}
|
||||
@@ -1,4 +1,11 @@
|
||||
import isParentOf from '../../Util/isParentOf.mjs'
|
||||
import isParentOf from '../../Util/isParentOf.js'
|
||||
|
||||
/**
|
||||
* @callback DomFilter
|
||||
* @param {string} nodeName
|
||||
* @param {Map<string, string>} attrs
|
||||
* @return {Map | null}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Default filter method (does nothing).
|
||||
35
src/Bindings/DomBinding/selection.js
Normal file
35
src/Bindings/DomBinding/selection.js
Normal file
@@ -0,0 +1,35 @@
|
||||
/* 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,84 +0,0 @@
|
||||
/* globals getSelection */
|
||||
|
||||
import { getRelativePosition, fromRelativePosition } from '../../Util/relativePosition.mjs'
|
||||
|
||||
let browserSelection = null
|
||||
let relativeSelection = null
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export let beforeTransactionSelectionFixer
|
||||
if (typeof getSelection !== 'undefined') {
|
||||
beforeTransactionSelectionFixer = function _beforeTransactionSelectionFixer (y, domBinding, transaction, remote) {
|
||||
if (!remote) {
|
||||
return
|
||||
}
|
||||
relativeSelection = { from: null, to: null, fromY: null, toY: null }
|
||||
browserSelection = getSelection()
|
||||
const anchorNode = browserSelection.anchorNode
|
||||
const anchorNodeType = domBinding.domToType.get(anchorNode)
|
||||
if (anchorNode !== null && anchorNodeType !== undefined) {
|
||||
relativeSelection.from = getRelativePosition(anchorNodeType, browserSelection.anchorOffset)
|
||||
relativeSelection.fromY = anchorNodeType._y
|
||||
}
|
||||
const focusNode = browserSelection.focusNode
|
||||
const focusNodeType = domBinding.domToType.get(focusNode)
|
||||
if (focusNode !== null && focusNodeType !== undefined) {
|
||||
relativeSelection.to = getRelativePosition(focusNodeType, browserSelection.focusOffset)
|
||||
relativeSelection.toY = focusNodeType._y
|
||||
}
|
||||
}
|
||||
} else {
|
||||
beforeTransactionSelectionFixer = function _fakeBeforeTransactionSelectionFixer () {}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export function afterTransactionSelectionFixer (y, domBinding, transaction, remote) {
|
||||
if (relativeSelection === null || !remote) {
|
||||
return
|
||||
}
|
||||
const to = relativeSelection.to
|
||||
const from = relativeSelection.from
|
||||
const fromY = relativeSelection.fromY
|
||||
const toY = relativeSelection.toY
|
||||
let shouldUpdate = false
|
||||
let anchorNode = browserSelection.anchorNode
|
||||
let anchorOffset = browserSelection.anchorOffset
|
||||
let focusNode = browserSelection.focusNode
|
||||
let focusOffset = browserSelection.focusOffset
|
||||
if (from !== null) {
|
||||
let sel = fromRelativePosition(fromY, from)
|
||||
if (sel !== null) {
|
||||
let node = domBinding.typeToDom.get(sel.type)
|
||||
let offset = sel.offset
|
||||
if (node !== anchorNode || offset !== anchorOffset) {
|
||||
anchorNode = node
|
||||
anchorOffset = offset
|
||||
shouldUpdate = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if (to !== null) {
|
||||
let sel = fromRelativePosition(toY, to)
|
||||
if (sel !== null) {
|
||||
let node = domBinding.typeToDom.get(sel.type)
|
||||
let offset = sel.offset
|
||||
if (node !== focusNode || offset !== focusOffset) {
|
||||
focusNode = node
|
||||
focusOffset = offset
|
||||
shouldUpdate = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if (shouldUpdate) {
|
||||
browserSelection.setBaseAndExtent(
|
||||
anchorNode,
|
||||
anchorOffset,
|
||||
focusNode,
|
||||
focusOffset
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
/* eslint-env browser */
|
||||
/* global getSelection */
|
||||
|
||||
import YXmlText from '../../Types/YXml/YXmlText.mjs'
|
||||
import YXmlHook from '../../Types/YXml/YXmlHook.mjs'
|
||||
import { removeDomChildrenUntilElementFound } from './util.mjs'
|
||||
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) {
|
||||
@@ -17,11 +18,17 @@ function findScrollReference (scrollingElement) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (anchor.nodeType === document.TEXT_NODE) {
|
||||
anchor = anchor.parentElement
|
||||
/**
|
||||
* @type {Element}
|
||||
*/
|
||||
let elem = anchor.parentElement
|
||||
if (anchor instanceof Element) {
|
||||
elem = anchor
|
||||
}
|
||||
return {
|
||||
elem,
|
||||
top: elem.getBoundingClientRect().top
|
||||
}
|
||||
const top = anchor.getBoundingClientRect().top
|
||||
return { elem: anchor, top: top }
|
||||
}
|
||||
}
|
||||
return null
|
||||
@@ -1,5 +1,12 @@
|
||||
|
||||
import domToType from './domToType.mjs'
|
||||
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.
|
||||
@@ -32,8 +39,8 @@ export function removeAssociation (domBinding, dom, type) {
|
||||
* 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
|
||||
* @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) {
|
||||
@@ -1,4 +1,4 @@
|
||||
import Binding from '../Binding.mjs'
|
||||
import Binding from '../Binding.js'
|
||||
|
||||
function typeObserver (event) {
|
||||
const quill = this.target
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
import Binding from '../Binding.mjs'
|
||||
import simpleDiff from '../../Util/simpleDiff.mjs'
|
||||
import { getRelativePosition, fromRelativePosition } from '../../Util/relativePosition.mjs'
|
||||
import Binding from '../Binding.js'
|
||||
import simpleDiff from '../../../lib/simpleDiff.js'
|
||||
import { getRelativePosition, fromRelativePosition } from '../../Util/relativePosition.js'
|
||||
|
||||
function typeObserver () {
|
||||
this._mutualExclude(() => {
|
||||
@@ -1,297 +0,0 @@
|
||||
import BinaryEncoder from './Util/Binary/Encoder.mjs'
|
||||
import BinaryDecoder from './Util/Binary/Decoder.mjs'
|
||||
|
||||
import { sendSyncStep1, readSyncStep1 } from './MessageHandler/syncStep1.mjs'
|
||||
import { readSyncStep2 } from './MessageHandler/syncStep2.mjs'
|
||||
import { integrateRemoteStructs } from './MessageHandler/integrateRemoteStructs.mjs'
|
||||
|
||||
import debug from 'debug'
|
||||
|
||||
// TODO: rename Connector
|
||||
|
||||
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._setContentReady()
|
||||
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(y, decoder)
|
||||
} 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
import BinaryEncoder from '../../Util/Binary/Encoder.mjs'
|
||||
/* global WebSocket */
|
||||
import NamedEventHandler from '../../Util/NamedEventHandler.mjs'
|
||||
import decodeMessage, { messageSS, messageSubscribe, messageStructs } from './decodeMessage.mjs'
|
||||
import { createMutualExclude } from '../../Util/mutualExclude.mjs'
|
||||
import { messageCheckUpdateCounter } from './decodeMessage.mjs'
|
||||
|
||||
export const STATE_DISCONNECTED = 0
|
||||
export const STATE_CONNECTED = 1
|
||||
|
||||
export default class WebsocketsConnector extends NamedEventHandler {
|
||||
constructor (url = 'ws://localhost:1234') {
|
||||
super()
|
||||
this.url = url
|
||||
this._state = STATE_DISCONNECTED
|
||||
this._socket = null
|
||||
this._rooms = new Map()
|
||||
this._connectToServer = true
|
||||
this._reconnectTimeout = 300
|
||||
this._mutualExclude = createMutualExclude()
|
||||
this._persistence = null
|
||||
this.connect()
|
||||
}
|
||||
|
||||
getRoom (roomName) {
|
||||
return this._rooms.get(roomName) || { y: null, roomName, localUpdateCounter: 1 }
|
||||
}
|
||||
|
||||
syncPersistence (persistence) {
|
||||
this._persistence = persistence
|
||||
if (this._state === STATE_CONNECTED) {
|
||||
persistence.getAllDocuments().then(docs => {
|
||||
const encoder = new BinaryEncoder()
|
||||
docs.forEach(doc => {
|
||||
messageCheckUpdateCounter(doc.roomName, encoder, doc.remoteUpdateCounter)
|
||||
});
|
||||
this.send(encoder)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
connectY (roomName, y) {
|
||||
let room = this._rooms.get(roomName)
|
||||
if (room !== undefined) {
|
||||
throw new Error('Room is already taken! There can be only one Yjs instance per roomName!')
|
||||
}
|
||||
this._rooms.set(roomName, {
|
||||
roomName,
|
||||
y,
|
||||
localUpdateCounter: 1
|
||||
})
|
||||
y.on('afterTransaction', (y, transaction) => {
|
||||
this._mutualExclude(() => {
|
||||
if (transaction.encodedStructsLen > 0) {
|
||||
const encoder = new BinaryEncoder()
|
||||
const room = this._rooms.get(roomName)
|
||||
messageStructs(roomName, y, encoder, transaction.encodedStructs, ++room.localUpdateCounter)
|
||||
this.send(encoder)
|
||||
}
|
||||
})
|
||||
})
|
||||
if (this._state === STATE_CONNECTED) {
|
||||
const encoder = new BinaryEncoder()
|
||||
messageSS(roomName, y, encoder)
|
||||
messageSubscribe(roomName, y, encoder)
|
||||
this.send(encoder)
|
||||
}
|
||||
}
|
||||
|
||||
_setState (state) {
|
||||
this._state = state
|
||||
this.emit('stateChanged', {
|
||||
state: this.state
|
||||
})
|
||||
}
|
||||
|
||||
get state () {
|
||||
return this._state === STATE_DISCONNECTED ? 'disconnected' : 'connected'
|
||||
}
|
||||
|
||||
_onOpen () {
|
||||
this._setState(STATE_CONNECTED)
|
||||
if (this._persistence === null) {
|
||||
const encoder = new BinaryEncoder()
|
||||
for (const [roomName, room] of this._rooms) {
|
||||
const y = room.y
|
||||
messageSS(roomName, y, encoder)
|
||||
messageSubscribe(roomName, y, encoder)
|
||||
}
|
||||
this.send(encoder)
|
||||
} else {
|
||||
this.syncPersistence(this._persistence)
|
||||
}
|
||||
}
|
||||
|
||||
send (encoder) {
|
||||
if (encoder.length > 0 && this._socket.readyState === WebSocket.OPEN) {
|
||||
this._socket.send(encoder.createBuffer())
|
||||
}
|
||||
}
|
||||
|
||||
_onClose () {
|
||||
this._setState(STATE_DISCONNECTED)
|
||||
this._socket = null
|
||||
if (this._connectToServer) {
|
||||
setTimeout(() => {
|
||||
if (this._connectToServer) {
|
||||
this.connect()
|
||||
}
|
||||
}, this._reconnectTimeout)
|
||||
this.connect()
|
||||
}
|
||||
}
|
||||
|
||||
_onMessage (message) {
|
||||
if (message.data.byteLength > 0) {
|
||||
const reply = decodeMessage(this, message.data, null, false, this._persistence)
|
||||
this.send(reply)
|
||||
}
|
||||
}
|
||||
|
||||
disconnect (code = 1000, reason = 'Client manually disconnected') {
|
||||
const socket = this._socket
|
||||
this._connectToServer = false
|
||||
socket.close(code, reason)
|
||||
}
|
||||
|
||||
connect () {
|
||||
if (this._socket === null) {
|
||||
const socket = new WebSocket(this.url)
|
||||
socket.binaryType = 'arraybuffer'
|
||||
this._socket = socket
|
||||
this._connectToServer = true
|
||||
// Connection opened
|
||||
socket.addEventListener('open', this._onOpen.bind(this))
|
||||
socket.addEventListener('close', this._onClose.bind(this))
|
||||
socket.addEventListener('message', this._onMessage.bind(this))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
import BinaryDecoder from '../../Util/Binary/Decoder.mjs'
|
||||
import BinaryEncoder from '../../Util/Binary/Encoder.mjs'
|
||||
import { readStateSet, writeStateSet } from '../../MessageHandler/stateSet.mjs'
|
||||
import { writeStructs } from '../../MessageHandler/syncStep1.mjs'
|
||||
import { writeDeleteSet, readDeleteSet } from '../../MessageHandler/deleteSet.mjs'
|
||||
import { integrateRemoteStructs } from '../../MessageHandler/integrateRemoteStructs.mjs'
|
||||
|
||||
const CONTENT_GET_SS = 4
|
||||
export function messageGetSS (roomName, y, encoder) {
|
||||
encoder.writeVarString(roomName)
|
||||
encoder.writeVarUint(CONTENT_GET_SS)
|
||||
}
|
||||
|
||||
const CONTENT_SUBSCRIBE = 3
|
||||
export function messageSubscribe (roomName, y, encoder) {
|
||||
encoder.writeVarString(roomName)
|
||||
encoder.writeVarUint(CONTENT_SUBSCRIBE)
|
||||
}
|
||||
|
||||
const CONTENT_SS = 0
|
||||
export function messageSS (roomName, y, encoder) {
|
||||
encoder.writeVarString(roomName)
|
||||
encoder.writeVarUint(CONTENT_SS)
|
||||
writeStateSet(y, encoder)
|
||||
}
|
||||
|
||||
const CONTENT_STRUCTS_DSS = 2
|
||||
export function messageStructsDSS (roomName, y, encoder, ss, updateCounter) {
|
||||
encoder.writeVarString(roomName)
|
||||
encoder.writeVarUint(CONTENT_STRUCTS_DSS)
|
||||
encoder.writeVarUint(updateCounter)
|
||||
const structsDS = new BinaryEncoder()
|
||||
writeStructs(y, structsDS, ss)
|
||||
writeDeleteSet(y, structsDS)
|
||||
encoder.writeVarUint(structsDS.length)
|
||||
encoder.writeBinaryEncoder(structsDS)
|
||||
}
|
||||
|
||||
const CONTENT_STRUCTS = 5
|
||||
export function messageStructs (roomName, y, encoder, structsBinaryEncoder, updateCounter) {
|
||||
encoder.writeVarString(roomName)
|
||||
encoder.writeVarUint(CONTENT_STRUCTS)
|
||||
encoder.writeVarUint(updateCounter)
|
||||
encoder.writeVarUint(structsBinaryEncoder.length)
|
||||
encoder.writeBinaryEncoder(structsBinaryEncoder)
|
||||
}
|
||||
|
||||
const CONTENT_CHECK_COUNTER = 6
|
||||
export function messageCheckUpdateCounter (roomName, encoder, updateCounter = 0) {
|
||||
encoder.writeVarString(roomName)
|
||||
encoder.writeVarUint(CONTENT_CHECK_COUNTER)
|
||||
encoder.writeVarUint(updateCounter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a client-message.
|
||||
*
|
||||
* A client-message consists of multiple message-elements that are concatenated without delimiter.
|
||||
* Each has the following structure:
|
||||
* - roomName
|
||||
* - content_type
|
||||
* - content (additional info that is encoded based on the value of content_type)
|
||||
*
|
||||
* The message is encoded until no more message-elements are available.
|
||||
*
|
||||
* @param {*} connector The connector that handles the connections
|
||||
* @param {*} message The binary encoded message
|
||||
* @param {*} ws The connection object
|
||||
*/
|
||||
export default function decodeMessage (connector, message, ws, isServer = false, persistence) {
|
||||
const decoder = new BinaryDecoder(message)
|
||||
const encoder = new BinaryEncoder()
|
||||
while (decoder.hasContent()) {
|
||||
const roomName = decoder.readVarString()
|
||||
const contentType = decoder.readVarUint()
|
||||
const room = connector.getRoom(roomName)
|
||||
const y = room.y
|
||||
switch (contentType) {
|
||||
case CONTENT_CHECK_COUNTER:
|
||||
const updateCounter = decoder.readVarUint()
|
||||
if (room.localUpdateCounter !== updateCounter) {
|
||||
messageGetSS(roomName, y, encoder)
|
||||
}
|
||||
connector.subscribe(roomName, ws)
|
||||
break
|
||||
case CONTENT_STRUCTS:
|
||||
console.log(`${roomName}: received update`)
|
||||
connector._mutualExclude(() => {
|
||||
const remoteUpdateCounter = decoder.readVarUint()
|
||||
persistence.setRemoteUpdateCounter(roomName, remoteUpdateCounter)
|
||||
const messageLen = decoder.readVarUint()
|
||||
if (y === null) {
|
||||
persistence._persistStructs(roomName, decoder.readArrayBuffer(messageLen))
|
||||
} else {
|
||||
y.transact(() => {
|
||||
integrateRemoteStructs(y, decoder)
|
||||
}, true)
|
||||
}
|
||||
})
|
||||
break
|
||||
case CONTENT_GET_SS:
|
||||
if (y !== null) {
|
||||
messageSS(roomName, y, encoder)
|
||||
} else {
|
||||
persistence._createYInstance(roomName).then(y => {
|
||||
const encoder = new BinaryEncoder()
|
||||
messageSS(roomName, y, encoder)
|
||||
connector.send(encoder, ws)
|
||||
})
|
||||
}
|
||||
break
|
||||
case CONTENT_SUBSCRIBE:
|
||||
connector.subscribe(roomName, ws)
|
||||
break
|
||||
case CONTENT_SS:
|
||||
// received state set
|
||||
// reply with missing content
|
||||
const ss = readStateSet(decoder)
|
||||
const sendStructsDSS = () => {
|
||||
if (y !== null) { // TODO: how to sync local content?
|
||||
const encoder = new BinaryEncoder()
|
||||
messageStructsDSS(roomName, y, encoder, ss, room.localUpdateCounter) // room.localUpdateHandler in case it changes
|
||||
if (isServer) {
|
||||
messageSS(roomName, y, encoder)
|
||||
}
|
||||
connector.send(encoder, ws)
|
||||
}
|
||||
}
|
||||
if (room.persistenceLoaded !== undefined) {
|
||||
room.persistenceLoaded.then(sendStructsDSS)
|
||||
} else {
|
||||
sendStructsDSS()
|
||||
}
|
||||
break
|
||||
case CONTENT_STRUCTS_DSS:
|
||||
console.log(`${roomName}: synced`)
|
||||
connector._mutualExclude(() => {
|
||||
const remoteUpdateCounter = decoder.readVarUint()
|
||||
persistence.setRemoteUpdateCounter(roomName, remoteUpdateCounter)
|
||||
const messageLen = decoder.readVarUint()
|
||||
if (y === null) {
|
||||
persistence._persistStructsDS(roomName, decoder.readArrayBuffer(messageLen))
|
||||
} else {
|
||||
y.transact(() => {
|
||||
integrateRemoteStructs(y, decoder)
|
||||
readDeleteSet(y, decoder)
|
||||
}, true)
|
||||
}
|
||||
})
|
||||
break
|
||||
default:
|
||||
console.error('Unexpected content type!')
|
||||
if (ws !== null) {
|
||||
ws.close() // TODO: specify reason
|
||||
}
|
||||
}
|
||||
}
|
||||
return encoder
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
import Y from '../../Y.mjs'
|
||||
import uws from 'uws'
|
||||
import BinaryEncoder from '../../Util/Binary/Encoder.mjs'
|
||||
import decodeMessage, { messageStructs } from './decodeMessage.mjs'
|
||||
import FilePersistence from '../../Persistences/FilePersistence.mjs'
|
||||
|
||||
const WebsocketsServer = uws.Server
|
||||
const persistence = new FilePersistence('.yjsPersisted')
|
||||
/**
|
||||
* Maps from room-name to ..
|
||||
* {
|
||||
* connections, // Set of ws-clients that listen to the room
|
||||
* y // Yjs instance that handles the room
|
||||
* }
|
||||
*/
|
||||
const rooms = new Map()
|
||||
/**
|
||||
* Maps from ws-connection to Set<roomName> - the set of connected roomNames
|
||||
*/
|
||||
const connections = new Map()
|
||||
const port = process.env.PORT || 1234
|
||||
const wss = new WebsocketsServer({
|
||||
port,
|
||||
perMessageDeflate: {}
|
||||
})
|
||||
|
||||
/**
|
||||
* Set of room names that are scheduled to be sweeped (destroyed because they don't have a connection anymore)
|
||||
*/
|
||||
const scheduledSweeps = new Set()
|
||||
/* TODO: enable sweeping
|
||||
setInterval(function sweepRoomes () {
|
||||
scheduledSweeps.forEach(roomName => {
|
||||
const room = rooms.get(roomName)
|
||||
if (room !== undefined) {
|
||||
if (room.connections.size === 0) {
|
||||
persistence.saveState(roomName, room.y).then(() => {
|
||||
if (room.connections.size === 0) {
|
||||
room.y.destroy()
|
||||
rooms.delete(roomName)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
scheduledSweeps.clear()
|
||||
}, 5000) */
|
||||
|
||||
const wsConnector = {
|
||||
send: (encoder, ws) => {
|
||||
const message = encoder.createBuffer()
|
||||
ws.send(message, null, null, true)
|
||||
},
|
||||
_mutualExclude: f => { f() },
|
||||
subscribe: function subscribe (roomName, ws) {
|
||||
let roomNames = connections.get(ws)
|
||||
if (roomNames === undefined) {
|
||||
roomNames = new Set()
|
||||
connections.set(ws, roomNames)
|
||||
}
|
||||
roomNames.add(roomName)
|
||||
const room = this.getRoom(roomName)
|
||||
room.connections.add(ws)
|
||||
},
|
||||
getRoom: function getRoom (roomName) {
|
||||
let room = rooms.get(roomName)
|
||||
if (room === undefined) {
|
||||
const y = new Y(roomName, null, null, { gc: true })
|
||||
const persistenceLoaded = persistence.readState(roomName, y)
|
||||
room = {
|
||||
name: roomName,
|
||||
connections: new Set(),
|
||||
y,
|
||||
persistenceLoaded,
|
||||
localUpdateCounter: 1
|
||||
}
|
||||
y.on('afterTransaction', (y, transaction) => {
|
||||
if (transaction.encodedStructsLen > 0) {
|
||||
// save to persistence
|
||||
persistence.saveUpdate(roomName, y, transaction.encodedStructs)
|
||||
// forward update to clients
|
||||
persistence._mutex(() => { // do not broadcast if persistence.readState is called
|
||||
const encoder = new BinaryEncoder()
|
||||
messageStructs(roomName, y, encoder, transaction.encodedStructs, ++room.localUpdateCounter)
|
||||
const message = encoder.createBuffer()
|
||||
// when changed, broakcast update to all connections
|
||||
room.connections.forEach(conn => {
|
||||
conn.send(message, null, null, true)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
rooms.set(roomName, room)
|
||||
}
|
||||
return room
|
||||
}
|
||||
}
|
||||
|
||||
wss.on('connection', (ws) => {
|
||||
ws.on('message', function onWSMessage (message) {
|
||||
if (message.byteLength > 0) {
|
||||
const reply = decodeMessage(wsConnector, message, ws, true, persistence)
|
||||
if (reply.length > 0) {
|
||||
ws.send(reply.createBuffer(), null, null, true)
|
||||
}
|
||||
}
|
||||
})
|
||||
ws.on('close', function onWSClose () {
|
||||
const roomNames = connections.get(ws)
|
||||
if (roomNames !== undefined) {
|
||||
roomNames.forEach(roomName => {
|
||||
const room = rooms.get(roomName)
|
||||
if (room !== undefined) {
|
||||
const connections = room.connections
|
||||
connections.delete(ws)
|
||||
if (connections.size === 0) {
|
||||
scheduledSweeps.add(roomName)
|
||||
}
|
||||
}
|
||||
})
|
||||
connections.delete(ws)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,32 +0,0 @@
|
||||
|
||||
import { writeStructs } from './syncStep1.mjs'
|
||||
import { integrateRemoteStructs } from './integrateRemoteStructs.mjs'
|
||||
import { readDeleteSet, writeDeleteSet } from './deleteSet.mjs'
|
||||
import BinaryEncoder from '../Util/Binary/Encoder.mjs'
|
||||
|
||||
/**
|
||||
* Read the Decoder and fill the Yjs instance with data in the decoder.
|
||||
*
|
||||
* @param {Y} y The Yjs instance
|
||||
* @param {BinaryDecoder} decoder The BinaryDecoder to read from.
|
||||
*/
|
||||
export function fromBinary (y, decoder) {
|
||||
y.transact(function () {
|
||||
integrateRemoteStructs(y, decoder)
|
||||
readDeleteSet(y, decoder)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode the Yjs model to binary format.
|
||||
*
|
||||
* @param {Y} y The Yjs instance
|
||||
* @return {BinaryEncoder} The encoder instance that can be transformed
|
||||
* to ArrayBuffer or Buffer.
|
||||
*/
|
||||
export function toBinary (y) {
|
||||
let encoder = new BinaryEncoder()
|
||||
writeStructs(y, encoder, new Map())
|
||||
writeDeleteSet(y, encoder)
|
||||
return encoder
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
import { deleteItemRange } from '../Struct/Delete.mjs'
|
||||
import ID from '../Util/ID/ID.mjs'
|
||||
|
||||
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, true)
|
||||
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]), true)
|
||||
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], true)
|
||||
}
|
||||
// for the rest.. just apply it
|
||||
for (; pos < dv.length; pos++) {
|
||||
d = dv[pos]
|
||||
deleteItemRange(y, user, d[0], d[1], true)
|
||||
// deletions.push([user, d[0], d[1], d[2]])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import BinaryDecoder from '../Util/Binary/Decoder.mjs'
|
||||
import { stringifyStructs } from './integrateRemoteStructs.mjs'
|
||||
import { stringifySyncStep1 } from './syncStep1.mjs'
|
||||
import { stringifySyncStep2 } from './syncStep2.mjs'
|
||||
import ID from '../Util/ID/ID.mjs'
|
||||
import RootID from '../Util/ID/RootID.mjs'
|
||||
import Y from '../Y.mjs'
|
||||
|
||||
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!')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper utility to convert an item to a readable format.
|
||||
*
|
||||
* @param {String} name The name of the item class (YText, ItemString, ..).
|
||||
* @param {Item} item The item instance.
|
||||
* @param {String} [append] Additional information to append to the returned
|
||||
* string.
|
||||
* @return {String} A readable string that represents the item object.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
export function logItemHelper (name, item, append) {
|
||||
const left = item._left !== null ? item._left._lastId : null
|
||||
const origin = item._origin !== null ? item._origin._lastId : null
|
||||
return `${name}(id:${logID(item._id)},left:${logID(left)},origin:${logID(origin)},right:${logID(item._right)},parent:${logID(item._parent)},parentSub:${item._parentSub}${append !== undefined ? ' - ' + append : ''})`
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
import BinaryEncoder from '../Util/Binary/Encoder.mjs'
|
||||
import { readStateSet, writeStateSet } from './stateSet.mjs'
|
||||
import { writeDeleteSet } from './deleteSet.mjs'
|
||||
import ID from '../Util/ID/ID.mjs'
|
||||
import { RootFakeUserID } from '../Util/ID/RootID.mjs'
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Write all Items that are not not included in ss to
|
||||
* the encoder object.
|
||||
*/
|
||||
export function writeStructs (y, encoder, 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) {
|
||||
const minBound = new ID(user, clock)
|
||||
const overlappingLeft = y.os.findPrev(minBound)
|
||||
const rightID = overlappingLeft === null ? null : overlappingLeft._id
|
||||
if (rightID !== null && rightID.user === user && rightID.clock + overlappingLeft._length > clock) {
|
||||
const struct = overlappingLeft._clonePartial(clock - rightID.clock)
|
||||
struct._toBinary(encoder)
|
||||
len++
|
||||
}
|
||||
y.os.iterate(minBound, 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(y, encoder, ss)
|
||||
writeDeleteSet(y, encoder)
|
||||
y.connector.send(senderConn.uid, encoder.createBuffer())
|
||||
senderConn.receivedSyncStep2 = true
|
||||
if (y.connector.role === 'slave') {
|
||||
sendSyncStep1(y.connector, sender)
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import { stringifyStructs, integrateRemoteStructs } from './integrateRemoteStructs.mjs'
|
||||
import { readDeleteSet } from './deleteSet.mjs'
|
||||
|
||||
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(y, decoder)
|
||||
readDeleteSet(y, decoder)
|
||||
y.connector._setSyncedWith(sender)
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
import BinaryEncoder from './Util/Binary/Encoder.mjs'
|
||||
import BinaryDecoder from './Util/Binary/Decoder.mjs'
|
||||
import { toBinary, fromBinary } from './MessageHandler/binaryEncode.mjs'
|
||||
import { integrateRemoteStructs } from './MessageHandler/integrateRemoteStructs.mjs'
|
||||
import { createMutualExclude } from './Util/mutualExclude.mjs'
|
||||
|
||||
function getFreshCnf () {
|
||||
let buffer = new BinaryEncoder()
|
||||
buffer.writeUint32(0)
|
||||
return {
|
||||
len: 0,
|
||||
buffer
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract persistence class.
|
||||
*/
|
||||
export default class AbstractPersistence {
|
||||
constructor (opts) {
|
||||
this.opts = opts
|
||||
this.ys = new Map()
|
||||
}
|
||||
|
||||
_init (y) {
|
||||
let cnf = this.ys.get(y)
|
||||
if (cnf === undefined) {
|
||||
cnf = getFreshCnf()
|
||||
cnf.mutualExclude = createMutualExclude()
|
||||
this.ys.set(y, cnf)
|
||||
return this.init(y).then(() => {
|
||||
y.on('afterTransaction', (y, transaction) => {
|
||||
let cnf = this.ys.get(y)
|
||||
if (cnf.len > 0) {
|
||||
cnf.buffer.setUint32(0, cnf.len)
|
||||
this.saveUpdate(y, cnf.buffer.createBuffer(), transaction)
|
||||
let _cnf = getFreshCnf()
|
||||
for (let key in _cnf) {
|
||||
cnf[key] = _cnf[key]
|
||||
}
|
||||
}
|
||||
})
|
||||
return this.retrieve(y)
|
||||
}).then(function () {
|
||||
return Promise.resolve(cnf)
|
||||
})
|
||||
} else {
|
||||
return Promise.resolve(cnf)
|
||||
}
|
||||
}
|
||||
deinit (y) {
|
||||
this.ys.delete(y)
|
||||
y.persistence = null
|
||||
}
|
||||
|
||||
destroy () {
|
||||
this.ys = null
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* ** Must be overwritten! **
|
||||
*/
|
||||
removePersistedData (room, destroyYjsInstances = true) {
|
||||
this.ys.forEach((cnf, y) => {
|
||||
if (y.room === room) {
|
||||
if (destroyYjsInstances) {
|
||||
y.destroy()
|
||||
} else {
|
||||
this.deinit(y)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/* overwrite */
|
||||
saveUpdate (buffer) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Save struct to update buffer.
|
||||
* saveUpdate is called when transaction ends
|
||||
*/
|
||||
saveStruct (y, struct) {
|
||||
let cnf = this.ys.get(y)
|
||||
if (cnf !== undefined) {
|
||||
cnf.mutualExclude(function () {
|
||||
struct._toBinary(cnf.buffer)
|
||||
cnf.len++
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/* overwrite */
|
||||
retrieve (y, model, updates) {
|
||||
let cnf = this.ys.get(y)
|
||||
if (cnf !== undefined) {
|
||||
cnf.mutualExclude(function () {
|
||||
y.transact(function () {
|
||||
if (model != null) {
|
||||
fromBinary(y, new BinaryDecoder(new Uint8Array(model)))
|
||||
}
|
||||
if (updates != null) {
|
||||
for (let i = 0; i < updates.length; i++) {
|
||||
integrateRemoteStructs(y, new BinaryDecoder(new Uint8Array(updates[i])))
|
||||
}
|
||||
}
|
||||
})
|
||||
y.emit('persistenceReady')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/* overwrite */
|
||||
persist (y) {
|
||||
return toBinary(y).createBuffer()
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import BinaryDecoder from '../Util/Binary/Decoder.mjs'
|
||||
import BinaryEncoder from '../Util/Binary/Encoder.mjs'
|
||||
import { createMutualExclude } from '../Util/mutualExclude.mjs'
|
||||
import { encodeUpdate, encodeStructsDS, decodePersisted } from './decodePersisted.mjs'
|
||||
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!
|
||||
@@ -23,9 +23,9 @@ export default class FilePersistence {
|
||||
return new Promise((resolve, reject) => {
|
||||
this._mutex(() => {
|
||||
const filePath = createFilePath(this, room)
|
||||
const updateMessage = new BinaryEncoder()
|
||||
const updateMessage = encoding.createEncoder()
|
||||
encodeUpdate(y, encodedStructs, updateMessage)
|
||||
fs.appendFile(filePath, Buffer.from(updateMessage.createBuffer()), (err) => {
|
||||
fs.appendFile(filePath, Buffer.from(encoding.toBuffer(updateMessage)), (err) => {
|
||||
if (err !== null) {
|
||||
reject(err)
|
||||
} else {
|
||||
@@ -37,10 +37,10 @@ export default class FilePersistence {
|
||||
}
|
||||
saveState (roomName, y) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const encoder = new BinaryEncoder()
|
||||
const encoder = encoding.createEncoder()
|
||||
encodeStructsDS(y, encoder)
|
||||
const filePath = createFilePath(this, roomName)
|
||||
fs.writeFile(filePath, Buffer.from(encoder.createBuffer()), (err) => {
|
||||
fs.writeFile(filePath, Buffer.from(encoding.toBuffer(encoder)), (err) => {
|
||||
if (err !== null) {
|
||||
reject(err)
|
||||
} else {
|
||||
@@ -61,7 +61,7 @@ export default class FilePersistence {
|
||||
this._mutex(() => {
|
||||
console.info(`unpacking data (${data.length})`)
|
||||
console.time('unpacking')
|
||||
decodePersisted(y, new BinaryDecoder(data))
|
||||
decodePersisted(y, decoding.createDecoder(data.buffer))
|
||||
console.timeEnd('unpacking')
|
||||
})
|
||||
resolve()
|
||||
@@ -1,12 +1,10 @@
|
||||
/* global indexedDB, location, BroadcastChannel */
|
||||
|
||||
import Y from '../Y.mjs'
|
||||
import { createMutualExclude } from '../Util/mutualExclude.mjs'
|
||||
import { decodePersisted, encodeStructsDS, encodeUpdate } from './decodePersisted.mjs'
|
||||
import BinaryDecoder from '../Util/Binary/Decoder.mjs'
|
||||
import BinaryEncoder from '../Util/Binary/Encoder.mjs'
|
||||
import { PERSIST_STRUCTS_DS } from './decodePersisted.mjs';
|
||||
import { PERSIST_UPDATE } from './decodePersisted.mjs';
|
||||
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
|
||||
*/
|
||||
@@ -1,6 +1,6 @@
|
||||
import { integrateRemoteStructs } from '../MessageHandler/integrateRemoteStructs.mjs'
|
||||
import { writeStructs } from '../MessageHandler/syncStep1.mjs'
|
||||
import { writeDeleteSet, readDeleteSet } from '../MessageHandler/deleteSet.mjs'
|
||||
import { integrateRemoteStructs } from '../MessageHandler/integrateRemoteStructs.js'
|
||||
import { writeStructs } from '../MessageHandler/syncStep1.js'
|
||||
import { writeDeleteSet, readDeleteSet } from '../MessageHandler/deleteSet.js'
|
||||
|
||||
export const PERSIST_UPDATE = 0
|
||||
/**
|
||||
@@ -39,10 +39,10 @@ export function decodePersisted (y, decoder) {
|
||||
const contentType = decoder.readVarUint()
|
||||
switch (contentType) {
|
||||
case PERSIST_UPDATE:
|
||||
integrateRemoteStructs(y, decoder)
|
||||
integrateRemoteStructs(decoder, y)
|
||||
break
|
||||
case PERSIST_STRUCTS_DS:
|
||||
integrateRemoteStructs(y, decoder)
|
||||
integrateRemoteStructs(decoder, y)
|
||||
readDeleteSet(y, decoder)
|
||||
break
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
import Tree from '../Util/Tree.mjs'
|
||||
import ID from '../Util/ID/ID.mjs'
|
||||
import Tree from '../../lib/Tree.js'
|
||||
import * as ID from '../Util/ID.js'
|
||||
|
||||
class DSNode {
|
||||
constructor (id, len, gc) {
|
||||
@@ -33,7 +33,7 @@ export default class DeleteStore extends Tree {
|
||||
mark (id, length, gc) {
|
||||
if (length === 0) return
|
||||
// Step 1. Unmark range
|
||||
const leftD = this.findWithUpperBound(new ID(id.user, id.clock - 1))
|
||||
const leftD = this.findWithUpperBound(ID.createID(id.user, id.clock - 1))
|
||||
// Resize left DSNode if necessary
|
||||
if (leftD !== null && leftD._id.user === id.user) {
|
||||
if (leftD._id.clock < id.clock && id.clock < leftD._id.clock + leftD.len) {
|
||||
@@ -41,19 +41,19 @@ export default class DeleteStore extends Tree {
|
||||
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(new ID(id.user, id.clock + length), leftD._id.clock + leftD.len - id.clock - length, leftD.gc))
|
||||
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
|
||||
const upper = new ID(id.user, id.clock + length - 1)
|
||||
const upper = ID.createID(id.user, id.clock + length - 1)
|
||||
const rightD = this.findWithUpperBound(upper)
|
||||
if (rightD !== null && rightD._id.user === id.user) {
|
||||
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
|
||||
const d = id.clock + length - rightD._id.clock
|
||||
rightD._id = new ID(rightD._id.user, rightD._id.clock + d)
|
||||
rightD._id = ID.createID(rightD._id.user, rightD._id.clock + d)
|
||||
rightD.len -= d
|
||||
}
|
||||
}
|
||||
@@ -72,7 +72,7 @@ export default class DeleteStore extends Tree {
|
||||
leftD.len += length
|
||||
newMark = leftD
|
||||
}
|
||||
const rightNext = this.find(new ID(id.user, id.clock + length))
|
||||
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
|
||||
@@ -1,8 +1,8 @@
|
||||
import Tree from '../Util/Tree.mjs'
|
||||
import RootID from '../Util/ID/RootID.mjs'
|
||||
import { getStruct } from '../Util/structReferences.mjs'
|
||||
import { logID } from '../MessageHandler/messageToString.mjs'
|
||||
import GC from '../Struct/GC.mjs'
|
||||
import Tree from '../../lib/Tree.js'
|
||||
import * as ID from '../Util/ID.js'
|
||||
import { getStruct } from '../Util/structReferences.js'
|
||||
import { stringifyID, stringifyItemID } from '../message.js'
|
||||
import GC from '../Struct/GC.js'
|
||||
|
||||
export default class OperationStore extends Tree {
|
||||
constructor (y) {
|
||||
@@ -14,18 +14,18 @@ export default class OperationStore extends Tree {
|
||||
this.iterate(null, null, function (item) {
|
||||
if (item.constructor === GC) {
|
||||
items.push({
|
||||
id: logID(item),
|
||||
id: stringifyItemID(item),
|
||||
content: item._length,
|
||||
deleted: 'GC'
|
||||
})
|
||||
} else {
|
||||
items.push({
|
||||
id: logID(item),
|
||||
origin: logID(item._origin === null ? null : item._origin._lastId),
|
||||
left: logID(item._left === null ? null : item._left._lastId),
|
||||
right: logID(item._right),
|
||||
right_origin: logID(item._right_origin),
|
||||
parent: logID(item._parent),
|
||||
id: stringifyItemID(item),
|
||||
origin: item._origin === null ? '()' : stringifyID(item._origin._lastId),
|
||||
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)
|
||||
@@ -36,7 +36,7 @@ export default class OperationStore extends Tree {
|
||||
}
|
||||
get (id) {
|
||||
let struct = this.find(id)
|
||||
if (struct === null && id instanceof RootID) {
|
||||
if (struct === null && id instanceof ID.RootID) {
|
||||
const Constr = getStruct(id.type)
|
||||
const y = this.y
|
||||
struct = new Constr()
|
||||
@@ -1,4 +1,8 @@
|
||||
import ID from '../Util/ID/ID.mjs'
|
||||
import * as ID from '../Util/ID.js'
|
||||
|
||||
/**
|
||||
* @typedef {Map<number, number>} StateSet
|
||||
*/
|
||||
|
||||
export default class StateStore {
|
||||
constructor (y) {
|
||||
@@ -18,14 +22,14 @@ export default class StateStore {
|
||||
const user = this.y.userID
|
||||
const state = this.getState(user)
|
||||
this.setState(user, state + len)
|
||||
return new ID(user, state)
|
||||
return ID.createID(user, state)
|
||||
}
|
||||
updateRemoteState (struct) {
|
||||
let user = struct._id.user
|
||||
let userState = this.state.get(user)
|
||||
while (struct !== null && struct._id.clock === userState) {
|
||||
userState += struct._length
|
||||
struct = this.y.os.get(new ID(user, userState))
|
||||
struct = this.y.os.get(ID.createID(user, userState))
|
||||
}
|
||||
this.state.set(user, userState)
|
||||
}
|
||||
@@ -1,31 +1,33 @@
|
||||
import { getStructReference } from '../Util/structReferences.mjs'
|
||||
import ID from '../Util/ID/ID.mjs'
|
||||
import { logID } from '../MessageHandler/messageToString.mjs'
|
||||
import { writeStructToTransaction } from '../Transaction.mjs'
|
||||
import { getStructReference } from '../Util/structReferences.js'
|
||||
import * as ID from '../Util/ID.js'
|
||||
import { stringifyID } from '../message.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
|
||||
* TODO: implement getItemCleanStartNode for better performance (only one lookup)
|
||||
* Delete all items in an ID-range.
|
||||
* Does not create delete operations!
|
||||
* TODO: implement getItemCleanStartNode for better performance (only one lookup).
|
||||
*/
|
||||
export function deleteItemRange (y, user, clock, range, gcChildren) {
|
||||
const createDelete = y.connector !== null && y.connector._forwardAppliedStructs
|
||||
let item = y.os.getItemCleanStart(new ID(user, clock))
|
||||
let item = y.os.getItemCleanStart(ID.createID(user, clock))
|
||||
if (item !== null) {
|
||||
if (!item._deleted) {
|
||||
item._splitAt(y, range)
|
||||
item._delete(y, createDelete, true)
|
||||
item._delete(y, false, true)
|
||||
}
|
||||
let itemLen = item._length
|
||||
range -= itemLen
|
||||
clock += itemLen
|
||||
if (range > 0) {
|
||||
let node = y.os.findNode(new ID(user, clock))
|
||||
while (node !== null && node.val !== null && range > 0 && node.val._id.equals(new ID(user, clock))) {
|
||||
let node = y.os.findNode(ID.createID(user, clock))
|
||||
while (node !== null && node.val !== null && range > 0 && node.val._id.equals(ID.createID(user, clock))) {
|
||||
const nodeVal = node.val
|
||||
if (!nodeVal._deleted) {
|
||||
nodeVal._splitAt(y, range)
|
||||
nodeVal._delete(y, createDelete, gcChildren)
|
||||
nodeVal._delete(y, false, gcChildren)
|
||||
}
|
||||
const nodeLen = nodeVal._length
|
||||
range -= nodeLen
|
||||
@@ -44,6 +46,13 @@ export function deleteItemRange (y, user, clock, range, gcChildren) {
|
||||
*/
|
||||
export default class Delete {
|
||||
constructor () {
|
||||
/**
|
||||
* @type {ID.ID}
|
||||
*/
|
||||
this._targetID = null
|
||||
/**
|
||||
* @type {import('./Item.js').default}
|
||||
*/
|
||||
this._target = null
|
||||
this._length = null
|
||||
}
|
||||
@@ -54,15 +63,18 @@ export default class Delete {
|
||||
*
|
||||
* This is called when data is received from a remote peer.
|
||||
*
|
||||
* @param {Y} y The Yjs instance that this Item belongs to.
|
||||
* @param {BinaryDecoder} decoder The decoder object to read data from.
|
||||
* @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) {
|
||||
// TODO: set target, and add it to missing if not found
|
||||
// There is an edge case in p2p networks!
|
||||
const targetID = decoder.readID()
|
||||
/**
|
||||
* @type {any}
|
||||
*/
|
||||
const targetID = ID.decode(decoder)
|
||||
this._targetID = targetID
|
||||
this._length = decoder.readVarUint()
|
||||
this._length = decoding.readVarUint(decoder)
|
||||
if (y.os.getItem(targetID) === null) {
|
||||
return [targetID]
|
||||
} else {
|
||||
@@ -77,12 +89,12 @@ export default class Delete {
|
||||
*
|
||||
* This is called when this Item is sent to a remote peer.
|
||||
*
|
||||
* @param {BinaryEncoder} encoder The encoder to write data to.
|
||||
* @param {encoding.Encoder} encoder The encoder to write data to.
|
||||
*/
|
||||
_toBinary (encoder) {
|
||||
encoder.writeUint8(getStructReference(this.constructor))
|
||||
encoder.writeID(this._targetID)
|
||||
encoder.writeVarUint(this._length)
|
||||
encoding.writeUint8(encoder, getStructReference(this.constructor))
|
||||
this._targetID.encode(encoder)
|
||||
encoding.writeVarUint(encoder, this._length)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -102,12 +114,6 @@ export default class Delete {
|
||||
// from remote
|
||||
const id = this._targetID
|
||||
deleteItemRange(y, id.user, id.clock, this._length, false)
|
||||
} else if (y.connector !== null) {
|
||||
// from local
|
||||
y.connector.broadcastStruct(this)
|
||||
}
|
||||
if (y.persistence !== null) {
|
||||
y.persistence.saveStruct(y, this)
|
||||
}
|
||||
writeStructToTransaction(y._transaction, this)
|
||||
}
|
||||
@@ -119,6 +125,6 @@ export default class Delete {
|
||||
* @private
|
||||
*/
|
||||
_logString () {
|
||||
return `Delete - target: ${logID(this._targetID)}, len: ${this._length}`
|
||||
return `Delete - target: ${stringifyID(this._targetID)}, len: ${this._length}`
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,15 @@
|
||||
import { getStructReference } from '../Util/structReferences.mjs'
|
||||
import { RootFakeUserID } from '../Util/ID/RootID.mjs'
|
||||
import ID from '../Util/ID/ID.mjs'
|
||||
import { writeStructToTransaction } from '../Transaction.mjs'
|
||||
import { getStructReference } from '../Util/structReferences.js'
|
||||
import * as ID from '../Util/ID.js'
|
||||
import { writeStructToTransaction } from '../Util/Transaction.js'
|
||||
import * as decoding from '../../lib/decoding.js'
|
||||
import * as encoding from '../../lib/encoding.js'
|
||||
|
||||
// TODO should have the same base class as Item
|
||||
export default class GC {
|
||||
constructor () {
|
||||
/**
|
||||
* @type {ID.ID}
|
||||
*/
|
||||
this._id = null
|
||||
this._length = 0
|
||||
}
|
||||
@@ -37,13 +41,7 @@ export default class GC {
|
||||
n._length += next._length
|
||||
y.os.delete(next._id)
|
||||
}
|
||||
if (id.user !== RootFakeUserID) {
|
||||
if (y.connector !== null && (y.connector._forwardAppliedStructs || id.user === y.userID)) {
|
||||
y.connector.broadcastStruct(this)
|
||||
}
|
||||
if (y.persistence !== null) {
|
||||
y.persistence.saveStruct(y, this)
|
||||
}
|
||||
if (id.user !== ID.RootFakeUserID) {
|
||||
writeStructToTransaction(y._transaction, this)
|
||||
}
|
||||
}
|
||||
@@ -54,13 +52,13 @@ export default class GC {
|
||||
*
|
||||
* This is called when this Item is sent to a remote peer.
|
||||
*
|
||||
* @param {BinaryEncoder} encoder The encoder to write data to.
|
||||
* @param {encoding.Encoder} encoder The encoder to write data to.
|
||||
* @private
|
||||
*/
|
||||
_toBinary (encoder) {
|
||||
encoder.writeUint8(getStructReference(this.constructor))
|
||||
encoder.writeID(this._id)
|
||||
encoder.writeVarUint(this._length)
|
||||
encoding.writeUint8(encoder, getStructReference(this.constructor))
|
||||
this._id.encode(encoder)
|
||||
encoding.writeVarUint(encoder, this._length)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -68,17 +66,20 @@ export default class GC {
|
||||
*
|
||||
* This is called when data is received from a remote peer.
|
||||
*
|
||||
* @param {Y} y The Yjs instance that this Item belongs to.
|
||||
* @param {BinaryDecoder} decoder The decoder object to read data from.
|
||||
* @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.
|
||||
* @private
|
||||
*/
|
||||
_fromBinary (y, decoder) {
|
||||
const id = decoder.readID()
|
||||
/**
|
||||
* @type {any}
|
||||
*/
|
||||
const id = ID.decode(decoder)
|
||||
this._id = id
|
||||
this._length = decoder.readVarUint()
|
||||
this._length = decoding.readVarUint(decoder)
|
||||
const missing = []
|
||||
if (y.ss.getState(id.user) < id.clock) {
|
||||
missing.push(new ID(id.user, id.clock - 1))
|
||||
missing.push(ID.createID(id.user, id.clock - 1))
|
||||
}
|
||||
return missing
|
||||
}
|
||||
@@ -89,7 +90,7 @@ export default class GC {
|
||||
|
||||
_clonePartial (diff) {
|
||||
const gc = new GC()
|
||||
gc._id = new ID(this._id.user, this._id.clock + diff)
|
||||
gc._id = ID.createID(this._id.user, this._id.clock + diff)
|
||||
gc._length = this._length - diff
|
||||
return gc
|
||||
}
|
||||
@@ -1,9 +1,15 @@
|
||||
import { getStructReference } from '../Util/structReferences.mjs'
|
||||
import ID from '../Util/ID/ID.mjs'
|
||||
import { default as RootID, RootFakeUserID } from '../Util/ID/RootID.mjs'
|
||||
import Delete from './Delete.mjs'
|
||||
import { transactionTypeChanged, writeStructToTransaction } from '../Transaction.mjs'
|
||||
import GC from './GC.mjs'
|
||||
import { getStructReference } from '../Util/structReferences.js'
|
||||
import * as ID from '../Util/ID.js'
|
||||
import Delete from './Delete.js'
|
||||
import { transactionTypeChanged, writeStructToTransaction } from '../Util/Transaction.js'
|
||||
import GC from './GC.js'
|
||||
import * as encoding from '../../lib/encoding.js'
|
||||
import * as decoding from '../../lib/decoding.js'
|
||||
import Y from '../Y.js'
|
||||
|
||||
/**
|
||||
* @typedef {import('./Type.js').default} YType
|
||||
*/
|
||||
|
||||
/**
|
||||
* @private
|
||||
@@ -15,7 +21,7 @@ import GC from './GC.mjs'
|
||||
*/
|
||||
export function splitHelper (y, a, b, diff) {
|
||||
const aID = a._id
|
||||
b._id = new ID(aID.user, aID.clock + diff)
|
||||
b._id = ID.createID(aID.user, aID.clock + diff)
|
||||
b._origin = a
|
||||
b._left = a
|
||||
b._right = a._right
|
||||
@@ -55,7 +61,7 @@ export default class Item {
|
||||
constructor () {
|
||||
/**
|
||||
* The uniqe identifier of this type.
|
||||
* @type {ID}
|
||||
* @type {ID.ID | ID.RootID}
|
||||
*/
|
||||
this._id = null
|
||||
/**
|
||||
@@ -87,7 +93,7 @@ export default class Item {
|
||||
* If the parent refers to this item with some kind of key (e.g. YMap, the
|
||||
* key is specified here. The key is then used to refer to the list in which
|
||||
* to insert this item. If `parentSub = null` type._start is the list in
|
||||
* which to insert to. Otherwise it is `parent._start`.
|
||||
* which to insert to. Otherwise it is `parent._map`.
|
||||
* @type {String}
|
||||
*/
|
||||
this._parentSub = null
|
||||
@@ -99,7 +105,7 @@ export default class Item {
|
||||
/**
|
||||
* If this type's effect is reundone this type refers to the type that undid
|
||||
* this operation.
|
||||
* @type {Item}
|
||||
* @type {YType}
|
||||
*/
|
||||
this._redone = null
|
||||
}
|
||||
@@ -110,7 +116,8 @@ export default class Item {
|
||||
* @private
|
||||
*/
|
||||
_copy () {
|
||||
return new this.constructor()
|
||||
const C = this.constructor
|
||||
return C()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -120,21 +127,36 @@ export default class Item {
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_redo (y) {
|
||||
_redo (y, redoitems) {
|
||||
if (this._redone !== null) {
|
||||
return this._redone
|
||||
}
|
||||
if (this._parent instanceof Y) {
|
||||
return
|
||||
}
|
||||
let struct = this._copy()
|
||||
let left = this._left
|
||||
let right = this
|
||||
let left, right
|
||||
if (this._parentSub === null) {
|
||||
// Is an array item. Insert at the old position
|
||||
left = this._left
|
||||
right = this
|
||||
} else {
|
||||
// Is a map item. Insert at the start
|
||||
left = null
|
||||
right = this._parent._map.get(this._parentSub)
|
||||
right._delete(y)
|
||||
}
|
||||
let parent = this._parent
|
||||
// make sure that parent is redone
|
||||
if (parent._deleted === true && parent._redone === null) {
|
||||
parent._redo(y)
|
||||
// try to undo parent if it will be undone anyway
|
||||
if (!redoitems.has(parent) || !parent._redo(y, redoitems)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if (parent._redone !== null) {
|
||||
parent = parent._redone
|
||||
// find next cloned items
|
||||
// find next cloned_redo items
|
||||
while (left !== null) {
|
||||
if (left._redone !== null && left._redone._parent === parent) {
|
||||
left = left._redone
|
||||
@@ -157,7 +179,7 @@ export default class Item {
|
||||
struct._parentSub = this._parentSub
|
||||
struct._integrate(y)
|
||||
this._redone = struct
|
||||
return struct
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -166,7 +188,11 @@ export default class Item {
|
||||
* @private
|
||||
*/
|
||||
get _lastId () {
|
||||
return new ID(this._id.user, this._id.clock + this._length - 1)
|
||||
/**
|
||||
* @type {any}
|
||||
*/
|
||||
const id = this._id
|
||||
return ID.createID(id.user, id.clock + this._length - 1)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -215,10 +241,11 @@ export default class Item {
|
||||
* @param {Y} y The Yjs instance
|
||||
* @param {boolean} createDelete Whether to propagate a message that this
|
||||
* Type was deleted.
|
||||
* @param {boolean} gcChildren
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_delete (y, createDelete = true) {
|
||||
_delete (y, createDelete = true, gcChildren) {
|
||||
if (!this._deleted) {
|
||||
this._deleted = true
|
||||
y.ds.mark(this._id, this._length, false)
|
||||
@@ -228,9 +255,6 @@ export default class Item {
|
||||
if (createDelete) {
|
||||
// broadcast and persists Delete
|
||||
del._integrate(y, true)
|
||||
} else if (y.persistence !== null) {
|
||||
// only persist Delete
|
||||
y.persistence.saveStruct(y, del)
|
||||
}
|
||||
transactionTypeChanged(y, this._parent, this._parentSub)
|
||||
y._transaction.deletedStructs.add(this)
|
||||
@@ -268,21 +292,30 @@ export default class Item {
|
||||
* * Add this struct to y.os
|
||||
* * Check if this is struct deleted
|
||||
*
|
||||
* @param {Y} y
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_integrate (y) {
|
||||
y._transaction.newTypes.add(this)
|
||||
/**
|
||||
* @type {any}
|
||||
*/
|
||||
const parent = this._parent
|
||||
/**
|
||||
* @type {any}
|
||||
*/
|
||||
const selfID = this._id
|
||||
const user = selfID === null ? y.userID : selfID.user
|
||||
const userState = y.ss.getState(user)
|
||||
if (selfID === null) {
|
||||
this._id = y.ss.getNextID(this._length)
|
||||
} else if (selfID.user === RootFakeUserID) {
|
||||
// nop
|
||||
} else if (selfID.user === ID.RootFakeUserID) {
|
||||
// is parent
|
||||
return
|
||||
} else if (selfID.clock < userState) {
|
||||
// already applied..
|
||||
return []
|
||||
return
|
||||
} else if (selfID.clock === userState) {
|
||||
y.ss.setState(selfID.user, userState + this._length)
|
||||
} else {
|
||||
@@ -292,7 +325,7 @@ export default class Item {
|
||||
if (!parent._deleted && !y._transaction.changedTypes.has(parent) && !y._transaction.newTypes.has(parent)) {
|
||||
// this is the first time parent is updated
|
||||
// or this types is new
|
||||
this._parent._beforeChange()
|
||||
parent._beforeChange()
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -316,9 +349,9 @@ export default class Item {
|
||||
if (this._left !== null) {
|
||||
o = this._left._right
|
||||
} else if (this._parentSub !== null) {
|
||||
o = this._parent._map.get(this._parentSub) || null
|
||||
o = parent._map.get(this._parentSub) || null
|
||||
} else {
|
||||
o = this._parent._start
|
||||
o = parent._start
|
||||
}
|
||||
let conflictingItems = new Set()
|
||||
let itemsBeforeOrigin = new Set()
|
||||
@@ -374,17 +407,11 @@ export default class Item {
|
||||
}
|
||||
}
|
||||
if (parent._deleted) {
|
||||
this._delete(y, false)
|
||||
this._delete(y, false, true)
|
||||
}
|
||||
y.os.put(this)
|
||||
transactionTypeChanged(y, parent, parentSub)
|
||||
if (this._id.user !== RootFakeUserID) {
|
||||
if (y.connector !== null && (y.connector._forwardAppliedStructs || this._id.user === y.userID)) {
|
||||
y.connector.broadcastStruct(this)
|
||||
}
|
||||
if (y.persistence !== null) {
|
||||
y.persistence.saveStruct(y, this)
|
||||
}
|
||||
if (this._id.user !== ID.RootFakeUserID) {
|
||||
writeStructToTransaction(y._transaction, this)
|
||||
}
|
||||
}
|
||||
@@ -395,12 +422,12 @@ export default class Item {
|
||||
*
|
||||
* This is called when this Item is sent to a remote peer.
|
||||
*
|
||||
* @param {BinaryEncoder} encoder The encoder to write data to.
|
||||
* @param {encoding.Encoder} encoder The encoder to write data to.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_toBinary (encoder) {
|
||||
encoder.writeUint8(getStructReference(this.constructor))
|
||||
encoding.writeUint8(encoder, getStructReference(this.constructor))
|
||||
let info = 0
|
||||
if (this._origin !== null) {
|
||||
info += 0b1 // origin is defined
|
||||
@@ -417,10 +444,10 @@ export default class Item {
|
||||
if (this._parentSub !== null) {
|
||||
info += 0b1000
|
||||
}
|
||||
encoder.writeUint8(info)
|
||||
encoder.writeID(this._id)
|
||||
encoding.writeUint8(encoder, info)
|
||||
this._id.encode(encoder)
|
||||
if (info & 0b1) {
|
||||
encoder.writeID(this._origin._lastId)
|
||||
this._origin._lastId.encode(encoder)
|
||||
}
|
||||
// TODO: remove
|
||||
/* see above
|
||||
@@ -429,14 +456,14 @@ export default class Item {
|
||||
}
|
||||
*/
|
||||
if (info & 0b100) {
|
||||
encoder.writeID(this._right_origin._id)
|
||||
this._right_origin._id.encode(encoder)
|
||||
}
|
||||
if ((info & 0b101) === 0) {
|
||||
// neither origin nor right is defined
|
||||
encoder.writeID(this._parent._id)
|
||||
this._parent._id.encode(encoder)
|
||||
}
|
||||
if (info & 0b1000) {
|
||||
encoder.writeVarString(JSON.stringify(this._parentSub))
|
||||
encoding.writeVarString(encoder, JSON.stringify(this._parentSub))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -446,19 +473,19 @@ export default class Item {
|
||||
* This is called when data is received from a remote peer.
|
||||
*
|
||||
* @param {Y} y The Yjs instance that this Item belongs to.
|
||||
* @param {BinaryDecoder} decoder The decoder object to read data from.
|
||||
* @param {decoding.Decoder} decoder The decoder object to read data from.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_fromBinary (y, decoder) {
|
||||
let missing = []
|
||||
const info = decoder.readUint8()
|
||||
const id = decoder.readID()
|
||||
const info = decoding.readUint8(decoder)
|
||||
const id = ID.decode(decoder)
|
||||
this._id = id
|
||||
// read origin
|
||||
if (info & 0b1) {
|
||||
// origin != null
|
||||
const originID = decoder.readID()
|
||||
const originID = ID.decode(decoder)
|
||||
// we have to query for left again because it might have been split/merged..
|
||||
const origin = y.os.getItemCleanEnd(originID)
|
||||
if (origin === null) {
|
||||
@@ -471,7 +498,7 @@ export default class Item {
|
||||
// read right
|
||||
if (info & 0b100) {
|
||||
// right != null
|
||||
const rightID = decoder.readID()
|
||||
const rightID = ID.decode(decoder)
|
||||
// we have to query for right again because it might have been split/merged..
|
||||
const right = y.os.getItemCleanStart(rightID)
|
||||
if (right === null) {
|
||||
@@ -484,11 +511,11 @@ export default class Item {
|
||||
// read parent
|
||||
if ((info & 0b101) === 0) {
|
||||
// neither origin nor right is defined
|
||||
const parentID = decoder.readID()
|
||||
const parentID = ID.decode(decoder)
|
||||
// parent does not change, so we don't have to search for it again
|
||||
if (this._parent === null) {
|
||||
let parent
|
||||
if (parentID.constructor === RootID) {
|
||||
if (parentID.constructor === ID.RootID) {
|
||||
parent = y.os.get(parentID)
|
||||
} else {
|
||||
parent = y.os.getItem(parentID)
|
||||
@@ -501,27 +528,17 @@ export default class Item {
|
||||
}
|
||||
} else if (this._parent === null) {
|
||||
if (this._origin !== null) {
|
||||
if (this._origin.constructor === GC) {
|
||||
// if origin is a gc, set parent also gc'd
|
||||
this._parent = this._origin
|
||||
} else {
|
||||
this._parent = this._origin._parent
|
||||
}
|
||||
this._parent = this._origin._parent
|
||||
} else if (this._right_origin !== null) {
|
||||
// if origin is a gc, set parent also gc'd
|
||||
if (this._right_origin.constructor === GC) {
|
||||
this._parent = this._right_origin
|
||||
} else {
|
||||
this._parent = this._right_origin._parent
|
||||
}
|
||||
this._parent = this._right_origin._parent
|
||||
}
|
||||
}
|
||||
if (info & 0b1000) {
|
||||
// TODO: maybe put this in read parent condition (you can also read parentsub from left/right)
|
||||
this._parentSub = JSON.parse(decoder.readVarString())
|
||||
this._parentSub = JSON.parse(decoding.readVarString(decoder))
|
||||
}
|
||||
if (y.ss.getState(id.user) < id.clock) {
|
||||
missing.push(new ID(id.user, id.clock - 1))
|
||||
if (id instanceof ID.ID && y.ss.getState(id.user) < id.clock) {
|
||||
missing.push(ID.createID(id.user, id.clock - 1))
|
||||
}
|
||||
return missing
|
||||
}
|
||||
@@ -1,5 +1,11 @@
|
||||
import Item from './Item.mjs'
|
||||
import { logItemHelper } from '../MessageHandler/messageToString.mjs'
|
||||
import Item from './Item.js'
|
||||
import { logItemHelper } from '../message.js'
|
||||
import * as encoding from '../../lib/encoding.js'
|
||||
import * as decoding from '../../lib/decoding.js'
|
||||
|
||||
/**
|
||||
* @typedef {import('../index.js').Y} Y
|
||||
*/
|
||||
|
||||
export default class ItemEmbed extends Item {
|
||||
constructor () {
|
||||
@@ -7,21 +13,28 @@ export default class ItemEmbed extends Item {
|
||||
this.embed = null
|
||||
}
|
||||
_copy (undeleteChildren, copyPosition) {
|
||||
let struct = super._copy(undeleteChildren, copyPosition)
|
||||
let struct = super._copy()
|
||||
struct.embed = this.embed
|
||||
return struct
|
||||
}
|
||||
get _length () {
|
||||
return 1
|
||||
}
|
||||
/**
|
||||
* @param {Y} y
|
||||
* @param {decoding.Decoder} decoder
|
||||
*/
|
||||
_fromBinary (y, decoder) {
|
||||
const missing = super._fromBinary(y, decoder)
|
||||
this.embed = JSON.parse(decoder.readVarString())
|
||||
this.embed = JSON.parse(decoding.readVarString(decoder))
|
||||
return missing
|
||||
}
|
||||
/**
|
||||
* @param {encoding.Encoder} encoder
|
||||
*/
|
||||
_toBinary (encoder) {
|
||||
super._toBinary(encoder)
|
||||
encoder.writeVarString(JSON.stringify(this.embed))
|
||||
encoding.writeVarString(encoder, JSON.stringify(this.embed))
|
||||
}
|
||||
/**
|
||||
* Transform this YXml Type to a readable format.
|
||||
@@ -1,5 +1,11 @@
|
||||
import Item from './Item.mjs'
|
||||
import { logItemHelper } from '../MessageHandler/messageToString.mjs'
|
||||
import Item from './Item.js'
|
||||
import { logItemHelper } from '../message.js'
|
||||
import * as encoding from '../../lib/encoding.js'
|
||||
import * as decoding from '../../lib/decoding.js'
|
||||
|
||||
/**
|
||||
* @typedef {import('../index.js').Y} Y
|
||||
*/
|
||||
|
||||
export default class ItemFormat extends Item {
|
||||
constructor () {
|
||||
@@ -8,7 +14,7 @@ export default class ItemFormat extends Item {
|
||||
this.value = null
|
||||
}
|
||||
_copy (undeleteChildren, copyPosition) {
|
||||
let struct = super._copy(undeleteChildren, copyPosition)
|
||||
let struct = super._copy()
|
||||
struct.key = this.key
|
||||
struct.value = this.value
|
||||
return struct
|
||||
@@ -19,16 +25,23 @@ export default class ItemFormat extends Item {
|
||||
get _countable () {
|
||||
return false
|
||||
}
|
||||
/**
|
||||
* @param {Y} y
|
||||
* @param {decoding.Decoder} decoder
|
||||
*/
|
||||
_fromBinary (y, decoder) {
|
||||
const missing = super._fromBinary(y, decoder)
|
||||
this.key = decoder.readVarString()
|
||||
this.value = JSON.parse(decoder.readVarString())
|
||||
this.key = decoding.readVarString(decoder)
|
||||
this.value = JSON.parse(decoding.readVarString(decoder))
|
||||
return missing
|
||||
}
|
||||
/**
|
||||
* @param {encoding.Encoder} encoder
|
||||
*/
|
||||
_toBinary (encoder) {
|
||||
super._toBinary(encoder)
|
||||
encoder.writeVarString(this.key)
|
||||
encoder.writeVarString(JSON.stringify(this.value))
|
||||
encoding.writeVarString(encoder, this.key)
|
||||
encoding.writeVarString(encoder, JSON.stringify(this.value))
|
||||
}
|
||||
/**
|
||||
* Transform this YXml Type to a readable format.
|
||||
@@ -1,5 +1,11 @@
|
||||
import Item, { splitHelper } from './Item.mjs'
|
||||
import { logItemHelper } from '../MessageHandler/messageToString.mjs'
|
||||
import Item, { splitHelper } from './Item.js'
|
||||
import { logItemHelper } from '../message.js'
|
||||
import * as encoding from '../../lib/encoding.js'
|
||||
import * as decoding from '../../lib/decoding.js'
|
||||
|
||||
/**
|
||||
* @typedef {import('../index.js').Y} Y
|
||||
*/
|
||||
|
||||
export default class ItemJSON extends Item {
|
||||
constructor () {
|
||||
@@ -14,12 +20,16 @@ export default class ItemJSON extends Item {
|
||||
get _length () {
|
||||
return this._content.length
|
||||
}
|
||||
/**
|
||||
* @param {Y} y
|
||||
* @param {decoding.Decoder} decoder
|
||||
*/
|
||||
_fromBinary (y, decoder) {
|
||||
let missing = super._fromBinary(y, decoder)
|
||||
let len = decoder.readVarUint()
|
||||
let len = decoding.readVarUint(decoder)
|
||||
this._content = new Array(len)
|
||||
for (let i = 0; i < len; i++) {
|
||||
const ctnt = decoder.readVarString()
|
||||
const ctnt = decoding.readVarString(decoder)
|
||||
let parsed
|
||||
if (ctnt === 'undefined') {
|
||||
parsed = undefined
|
||||
@@ -30,10 +40,13 @@ export default class ItemJSON extends Item {
|
||||
}
|
||||
return missing
|
||||
}
|
||||
/**
|
||||
* @param {encoding.Encoder} encoder
|
||||
*/
|
||||
_toBinary (encoder) {
|
||||
super._toBinary(encoder)
|
||||
let len = this._content.length
|
||||
encoder.writeVarUint(len)
|
||||
encoding.writeVarUint(encoder, len)
|
||||
for (let i = 0; i < len; i++) {
|
||||
let encoded
|
||||
let content = this._content[i]
|
||||
@@ -42,7 +55,7 @@ export default class ItemJSON extends Item {
|
||||
} else {
|
||||
encoded = JSON.stringify(content)
|
||||
}
|
||||
encoder.writeVarString(encoded)
|
||||
encoding.writeVarString(encoder, encoded)
|
||||
}
|
||||
}
|
||||
/**
|
||||
@@ -1,5 +1,11 @@
|
||||
import Item, { splitHelper } from './Item.mjs'
|
||||
import { logItemHelper } from '../MessageHandler/messageToString.mjs'
|
||||
import Item, { splitHelper } from './Item.js'
|
||||
import { logItemHelper } from '../message.js'
|
||||
import * as encoding from '../../lib/encoding.js'
|
||||
import * as decoding from '../../lib/decoding.js'
|
||||
|
||||
/**
|
||||
* @typedef {import('../index.js').Y} Y
|
||||
*/
|
||||
|
||||
export default class ItemString extends Item {
|
||||
constructor () {
|
||||
@@ -14,14 +20,21 @@ export default class ItemString extends Item {
|
||||
get _length () {
|
||||
return this._content.length
|
||||
}
|
||||
/**
|
||||
* @param {Y} y
|
||||
* @param {decoding.Decoder} decoder
|
||||
*/
|
||||
_fromBinary (y, decoder) {
|
||||
let missing = super._fromBinary(y, decoder)
|
||||
this._content = decoder.readVarString()
|
||||
this._content = decoding.readVarString(decoder)
|
||||
return missing
|
||||
}
|
||||
/**
|
||||
* @param {encoding.Encoder} encoder
|
||||
*/
|
||||
_toBinary (encoder) {
|
||||
super._toBinary(encoder)
|
||||
encoder.writeVarString(this._content)
|
||||
encoding.writeVarString(encoder, this._content)
|
||||
}
|
||||
/**
|
||||
* Transform this YXml Type to a readable format.
|
||||
@@ -1,6 +1,11 @@
|
||||
import Item from './Item.mjs'
|
||||
import EventHandler from '../Util/EventHandler.mjs'
|
||||
import ID from '../Util/ID/ID.mjs'
|
||||
import Item from './Item.js'
|
||||
import EventHandler from '../Util/EventHandler.js'
|
||||
import { createID } from '../Util/ID.js'
|
||||
import YEvent from '../Util/YEvent.js'
|
||||
|
||||
/**
|
||||
* @typedef {import("../Y.js").default} Y
|
||||
*/
|
||||
|
||||
// restructure children as if they were inserted one after another
|
||||
function integrateChildren (y, start) {
|
||||
@@ -22,7 +27,7 @@ export function getListItemIDByPosition (type, i) {
|
||||
if (!n._deleted) {
|
||||
if (pos <= i && i < pos + n._length) {
|
||||
const id = n._id
|
||||
return new ID(id.user, id.clock + i - pos)
|
||||
return createID(id.user, id.clock + i - pos)
|
||||
}
|
||||
pos++
|
||||
}
|
||||
@@ -61,7 +66,7 @@ export default class Type extends Item {
|
||||
* console.log(path) // might look like => [2, 'key1']
|
||||
* child === type.get(path[0]).get(path[1])
|
||||
*
|
||||
* @param {YType} type Type target
|
||||
* @param {Type | Y | any} type Type target
|
||||
* @return {Array<string>} Path to the target
|
||||
*/
|
||||
getPathTo (type) {
|
||||
@@ -91,6 +96,14 @@ export default class Type extends Item {
|
||||
return path
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Creates YArray Event and calls observers.
|
||||
*/
|
||||
_callObserver (transaction, parentSubs, remote) {
|
||||
this._callEventHandler(transaction, new YEvent(this))
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Call event listeners with an event. This will also add an event to all
|
||||
@@ -99,6 +112,9 @@ export default class Type extends Item {
|
||||
_callEventHandler (transaction, event) {
|
||||
const changedParentTypes = transaction.changedParentTypes
|
||||
this._eventHandler.callEventListeners(transaction, event)
|
||||
/**
|
||||
* @type {any}
|
||||
*/
|
||||
let type = this
|
||||
while (type !== this._y) {
|
||||
let events = changedParentTypes.get(type)
|
||||
@@ -183,7 +199,7 @@ export default class Type extends Item {
|
||||
this._start = null
|
||||
integrateChildren(y, start)
|
||||
}
|
||||
// integrate map children
|
||||
// integrate map children_integrate
|
||||
const map = this._map
|
||||
this._map = new Map()
|
||||
for (let t of map.values()) {
|
||||
@@ -206,6 +222,12 @@ export default class Type extends Item {
|
||||
super._gc(y)
|
||||
}
|
||||
|
||||
/**
|
||||
* @abstract
|
||||
* @return {Object | Array | number | string}
|
||||
*/
|
||||
toJSON () {}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Mark this Item as deleted.
|
||||
@@ -213,7 +235,7 @@ export default class Type extends Item {
|
||||
* @param {Y} y The Yjs instance
|
||||
* @param {boolean} createDelete Whether to propagate a message that this
|
||||
* Type was deleted.
|
||||
* @param {boolean} [gcChildren=y._hasUndoManager===false] Whether to garbage
|
||||
* @param {boolean} [gcChildren=(y._hasUndoManager===false)] Whether to garbage
|
||||
* collect the children of this type.
|
||||
*/
|
||||
_delete (y, createDelete, gcChildren) {
|
||||
@@ -1,8 +1,14 @@
|
||||
import Type from '../../Struct/Type.mjs'
|
||||
import ItemJSON from '../../Struct/ItemJSON.mjs'
|
||||
import ItemString from '../../Struct/ItemString.mjs'
|
||||
import { logID, logItemHelper } from '../../MessageHandler/messageToString.mjs'
|
||||
import YEvent from '../../Util/YEvent.mjs'
|
||||
import Type from '../../Struct/Type.js'
|
||||
import ItemJSON from '../../Struct/ItemJSON.js'
|
||||
import ItemString from '../../Struct/ItemString.js'
|
||||
import { stringifyItemID, logItemHelper } from '../../message.js'
|
||||
import YEvent from '../../Util/YEvent.js'
|
||||
|
||||
/**
|
||||
* @typedef {import('../../Struct/Item.js').default} Item
|
||||
* @typedef {import('../../Util/Transaction.js').default} Transaction
|
||||
* @typedef {import('../../Y.js').default} Y
|
||||
*/
|
||||
|
||||
/**
|
||||
* Event that describes the changes on a YArray
|
||||
@@ -76,7 +82,7 @@ export default class YArray extends Type {
|
||||
/**
|
||||
* Returns the i-th element from a YArray.
|
||||
*
|
||||
* @param {Integer} index The index of the element to return from the YArray
|
||||
* @param {number} index The index of the element to return from the YArray
|
||||
*/
|
||||
get (index) {
|
||||
let n = this._start
|
||||
@@ -112,11 +118,7 @@ export default class YArray extends Type {
|
||||
toJSON () {
|
||||
return this.map(c => {
|
||||
if (c instanceof Type) {
|
||||
if (c.toJSON !== null) {
|
||||
return c.toJSON()
|
||||
} else {
|
||||
return c.toString()
|
||||
}
|
||||
return c.toJSON()
|
||||
}
|
||||
return c
|
||||
})
|
||||
@@ -211,8 +213,8 @@ export default class YArray extends Type {
|
||||
/**
|
||||
* Deletes elements starting from an index.
|
||||
*
|
||||
* @param {Integer} index Index at which to start deleting elements
|
||||
* @param {Integer} length The number of elements to remove. Defaults to 1.
|
||||
* @param {number} index Index at which to start deleting elements
|
||||
* @param {number} length The number of elements to remove. Defaults to 1.
|
||||
*/
|
||||
delete (index, length = 1) {
|
||||
this._y.transact(() => {
|
||||
@@ -318,7 +320,7 @@ export default class YArray extends Type {
|
||||
* // Insert numbers 1, 2 at position 1
|
||||
* yarray.insert(2, [1, 2])
|
||||
*
|
||||
* @param {Integer} index The index to insert content at.
|
||||
* @param {number} index The index to insert content at.
|
||||
* @param {Array} content The array of content
|
||||
*/
|
||||
insert (index, content) {
|
||||
@@ -373,6 +375,6 @@ export default class YArray extends Type {
|
||||
* @private
|
||||
*/
|
||||
_logString () {
|
||||
return logItemHelper('YArray', this, `start:${logID(this._start)}"`)
|
||||
return logItemHelper('YArray', this, `start:${stringifyItemID(this._start)}"`)
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,13 @@
|
||||
import Item from '../../Struct/Item.mjs'
|
||||
import Type from '../../Struct/Type.mjs'
|
||||
import ItemJSON from '../../Struct/ItemJSON.mjs'
|
||||
import { logItemHelper } from '../../MessageHandler/messageToString.mjs'
|
||||
import YEvent from '../../Util/YEvent.mjs'
|
||||
import Item from '../../Struct/Item.js'
|
||||
import Type from '../../Struct/Type.js'
|
||||
import ItemJSON from '../../Struct/ItemJSON.js'
|
||||
import { logItemHelper } from '../../message.js'
|
||||
import YEvent from '../../Util/YEvent.js'
|
||||
|
||||
/**
|
||||
* @typedef {import('../../Y.js').encodable} encodable
|
||||
* @typedef {import('../../Struct/Type.js')} YType
|
||||
*/
|
||||
|
||||
/**
|
||||
* Event that describes the changes on a YMap.
|
||||
@@ -1,8 +1,8 @@
|
||||
import ItemEmbed from '../../Struct/ItemEmbed.mjs'
|
||||
import ItemString from '../../Struct/ItemString.mjs'
|
||||
import ItemFormat from '../../Struct/ItemFormat.mjs'
|
||||
import { logItemHelper } from '../../MessageHandler/messageToString.mjs'
|
||||
import { YArrayEvent, default as YArray } from '../YArray/YArray.mjs'
|
||||
import ItemEmbed from '../../Struct/ItemEmbed.js'
|
||||
import ItemString from '../../Struct/ItemString.js'
|
||||
import ItemFormat from '../../Struct/ItemFormat.js'
|
||||
import { logItemHelper } from '../../message.js'
|
||||
import { YArrayEvent, default as YArray } from '../YArray/YArray.js'
|
||||
|
||||
/**
|
||||
* @private
|
||||
@@ -254,7 +254,7 @@ function deleteText (y, length, parent, left, right, currentAttributes) {
|
||||
* @typedef {Array<Object>} Delta
|
||||
*/
|
||||
|
||||
/**
|
||||
/**
|
||||
* Attributes that can be assigned to a selection of text.
|
||||
*
|
||||
* @example
|
||||
@@ -304,6 +304,9 @@ class YTextEvent extends YArrayEvent {
|
||||
let deleteLen = 0
|
||||
const addOp = function addOp () {
|
||||
if (action !== null) {
|
||||
/**
|
||||
* @type {any}
|
||||
*/
|
||||
let op
|
||||
switch (action) {
|
||||
case 'delete':
|
||||
@@ -483,6 +486,9 @@ export default class YText extends YArray {
|
||||
*/
|
||||
toString () {
|
||||
let str = ''
|
||||
/**
|
||||
* @type {any}
|
||||
*/
|
||||
let n = this._start
|
||||
while (n !== null) {
|
||||
if (!n._deleted && n._countable) {
|
||||
@@ -529,6 +535,9 @@ export default class YText extends YArray {
|
||||
let ops = []
|
||||
let currentAttributes = new Map()
|
||||
let str = ''
|
||||
/**
|
||||
* @type {any}
|
||||
*/
|
||||
let n = this._start
|
||||
function packStr () {
|
||||
if (str.length > 0) {
|
||||
@@ -568,12 +577,11 @@ export default class YText extends YArray {
|
||||
/**
|
||||
* Insert text at a given index.
|
||||
*
|
||||
* @param {Integer} index The index at which to start inserting.
|
||||
* @param {number} index The index at which to start inserting.
|
||||
* @param {String} text The text to insert at the specified position.
|
||||
* @param {TextAttributes} attributes Optionally define some formatting
|
||||
* information to apply on the inserted
|
||||
* Text.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
insert (index, text, attributes = {}) {
|
||||
@@ -589,7 +597,7 @@ export default class YText extends YArray {
|
||||
/**
|
||||
* Inserts an embed at a index.
|
||||
*
|
||||
* @param {Integer} index The index to insert the embed at.
|
||||
* @param {number} index The index to insert the embed at.
|
||||
* @param {Object} embed The Object that represents the embed.
|
||||
* @param {TextAttributes} attributes Attribute information to apply on the
|
||||
* embed
|
||||
@@ -609,8 +617,8 @@ export default class YText extends YArray {
|
||||
/**
|
||||
* Deletes text starting from an index.
|
||||
*
|
||||
* @param {Integer} index Index at which to start deleting.
|
||||
* @param {Integer} length The number of characters to remove. Defaults to 1.
|
||||
* @param {number} index Index at which to start deleting.
|
||||
* @param {number} length The number of characters to remove. Defaults to 1.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
@@ -627,8 +635,8 @@ export default class YText extends YArray {
|
||||
/**
|
||||
* Assigns properties to a range of text.
|
||||
*
|
||||
* @param {Integer} index The position where to start formatting.
|
||||
* @param {Integer} length The amount of characters to assign properties to.
|
||||
* @param {number} index The position where to start formatting.
|
||||
* @param {number} length The amount of characters to assign properties to.
|
||||
* @param {TextAttributes} attributes Attribute information to apply on the
|
||||
* text.
|
||||
*
|
||||
@@ -1,6 +1,12 @@
|
||||
import YMap from '../YMap/YMap.mjs'
|
||||
import YXmlFragment from './YXmlFragment.mjs'
|
||||
import { createAssociation } from '../../Bindings/DomBinding/util.mjs'
|
||||
import YMap from '../YMap/YMap.js'
|
||||
import YXmlFragment from './YXmlFragment.js'
|
||||
import { createAssociation } from '../../Bindings/DomBinding/util.js'
|
||||
import * as encoding from '../../../lib/encoding.js'
|
||||
import * as decoding from '../../../lib/decoding.js'
|
||||
|
||||
/**
|
||||
* @typedef {import('../../Y.js').default} Y
|
||||
*/
|
||||
|
||||
/**
|
||||
* An YXmlElement imitates the behavior of a
|
||||
@@ -8,8 +14,6 @@ import { createAssociation } from '../../Bindings/DomBinding/util.mjs'
|
||||
*
|
||||
* * An YXmlElement has attributes (key value pairs)
|
||||
* * An YXmlElement has childElements that must inherit from YXmlElement
|
||||
*
|
||||
* @param {String} nodeName Node name
|
||||
*/
|
||||
export default class YXmlElement extends YXmlFragment {
|
||||
constructor (nodeName = 'UNDEFINED') {
|
||||
@@ -34,11 +38,11 @@ export default class YXmlElement extends YXmlFragment {
|
||||
* This is called when data is received from a remote peer.
|
||||
*
|
||||
* @param {Y} y The Yjs instance that this Item belongs to.
|
||||
* @param {BinaryDecoder} decoder The decoder object to read data from.
|
||||
* @param {decoding.Decoder} decoder The decoder object to read data from.
|
||||
*/
|
||||
_fromBinary (y, decoder) {
|
||||
const missing = super._fromBinary(y, decoder)
|
||||
this.nodeName = decoder.readVarString()
|
||||
this.nodeName = decoding.readVarString(decoder)
|
||||
return missing
|
||||
}
|
||||
|
||||
@@ -48,13 +52,13 @@ export default class YXmlElement extends YXmlFragment {
|
||||
*
|
||||
* This is called when this Item is sent to a remote peer.
|
||||
*
|
||||
* @param {BinaryEncoder} encoder The encoder to write data to.
|
||||
* @param {encoding.Encoder} encoder The encoder to write data to.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_toBinary (encoder) {
|
||||
super._toBinary(encoder)
|
||||
encoder.writeVarString(this.nodeName)
|
||||
encoding.writeVarString(encoder, this.nodeName)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -164,9 +168,9 @@ export default class YXmlElement extends YXmlFragment {
|
||||
* @param {Document} [_document=document] The document object (you must define
|
||||
* this when calling this method in
|
||||
* nodejs)
|
||||
* @param {Object<key:hookDefinition>} [hooks={}] Optional property to customize how hooks
|
||||
* @param {Object<string, any>} [hooks={}] Optional property to customize how hooks
|
||||
* are presented in the DOM
|
||||
* @param {DomBinding} [binding] You should not set this property. This is
|
||||
* @param {import('../../Bindings/DomBinding/DomBinding.js').default} [binding] You should not set this property. This is
|
||||
* used if DomBinding wants to create a
|
||||
* association to the created DOM type.
|
||||
* @return {Element} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}
|
||||
@@ -187,4 +191,12 @@ export default class YXmlElement extends YXmlFragment {
|
||||
}
|
||||
}
|
||||
|
||||
YXmlFragment._YXmlElement = YXmlElement
|
||||
// reassign yxmlfragment to {any} type to prevent warnings
|
||||
// assign yxmlelement to YXmlFragment so it has a reference to YXmlElement.
|
||||
|
||||
/**
|
||||
* @type {any}
|
||||
*/
|
||||
const _reasgn = YXmlFragment
|
||||
|
||||
_reasgn._YXmlElement = YXmlElement
|
||||
@@ -1,4 +1,9 @@
|
||||
import YEvent from '../../Util/YEvent.mjs'
|
||||
import YEvent from '../../Util/YEvent.js'
|
||||
|
||||
/**
|
||||
* @typedef {import('../../Struct/Type.js').default} YType
|
||||
* @typedef {import('../../Util/Transaction.js').default} Transaction
|
||||
*/
|
||||
|
||||
/**
|
||||
* An Event that describes changes on a YXml Element or Yxml Fragment
|
||||
@@ -1,9 +1,15 @@
|
||||
import { createAssociation } from '../../Bindings/DomBinding/util.mjs'
|
||||
import YXmlTreeWalker from './YXmlTreeWalker.mjs'
|
||||
import { createAssociation } from '../../Bindings/DomBinding/util.js'
|
||||
import YXmlTreeWalker from './YXmlTreeWalker.js'
|
||||
|
||||
import YArray from '../YArray/YArray.mjs'
|
||||
import YXmlEvent from './YXmlEvent.mjs'
|
||||
import { logItemHelper } from '../../MessageHandler/messageToString.mjs'
|
||||
import YArray from '../YArray/YArray.js'
|
||||
import YXmlEvent from './YXmlEvent.js'
|
||||
import { logItemHelper } from '../../message.js'
|
||||
|
||||
/**
|
||||
* @typedef {import('./YXmlElement.js').default} YXmlElement
|
||||
* @typedef {import('../../Bindings/DomBinding/DomBinding.js').default} DomBinding
|
||||
* @typedef {import('../../Y.js').default} Y
|
||||
*/
|
||||
|
||||
/**
|
||||
* Dom filter function.
|
||||
@@ -48,7 +54,7 @@ export default class YXmlFragment extends YArray {
|
||||
* @param {Function} filter Function that is called on each child element and
|
||||
* returns a Boolean indicating whether the child
|
||||
* is to be included in the subtree.
|
||||
* @return {TreeWalker} A subtree and a position within it.
|
||||
* @return {YXmlTreeWalker} A subtree and a position within it.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
@@ -67,7 +73,7 @@ export default class YXmlFragment extends YArray {
|
||||
* - attribute
|
||||
*
|
||||
* @param {CSS_Selector} query The query on the children.
|
||||
* @return {?YXmlElement} The first element that matches the query or null.
|
||||
* @return {?import('./YXmlElement.js')} The first element that matches the query or null.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
@@ -116,29 +122,13 @@ export default class YXmlFragment extends YArray {
|
||||
return this.map(xml => xml.toString()).join('')
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Unbind from Dom and mark this Item as deleted.
|
||||
*
|
||||
* @param {Y} y The Yjs instance
|
||||
* @param {boolean} createDelete Whether to propagate a message that this
|
||||
* Type was deleted.
|
||||
* @param {boolean} [gcChildren=y._hasUndoManager===false] Whether to garbage
|
||||
* collect the children of this type.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_delete (y, createDelete, gcChildren) {
|
||||
super._delete(y, createDelete, gcChildren)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Dom Element that mirrors this YXmlElement.
|
||||
*
|
||||
* @param {Document} [_document=document] The document object (you must define
|
||||
* this when calling this method in
|
||||
* nodejs)
|
||||
* @param {Object<key:hookDefinition>} [hooks={}] Optional property to customize how hooks
|
||||
* @param {Object.<string, any>} [hooks={}] Optional property to customize how hooks
|
||||
* are presented in the DOM
|
||||
* @param {DomBinding} [binding] You should not set this property. This is
|
||||
* used if DomBinding wants to create a
|
||||
@@ -1,5 +1,12 @@
|
||||
import YMap from '../YMap/YMap.mjs'
|
||||
import { createAssociation } from '../../Bindings/DomBinding/util.mjs'
|
||||
import YMap from '../YMap/YMap.js'
|
||||
import { createAssociation } from '../../Bindings/DomBinding/util.js'
|
||||
import * as encoding from '../../../lib/encoding.js'
|
||||
import * as decoding from '../../../lib/decoding.js'
|
||||
|
||||
/**
|
||||
* @typedef {import('../../Bindings/DomBinding/DomBinding.js').default} DomBinding
|
||||
* @typedef {import('../../Y.js').default} Y
|
||||
*/
|
||||
|
||||
/**
|
||||
* You can manage binding to a custom type with YXmlHook.
|
||||
@@ -35,7 +42,7 @@ export default class YXmlHook extends YMap {
|
||||
* @param {Document} [_document=document] The document object (you must define
|
||||
* this when calling this method in
|
||||
* nodejs)
|
||||
* @param {Object<key:hookDefinition>} [hooks] Optional property to customize how hooks
|
||||
* @param {Object.<string, any>} [hooks] Optional property to customize how hooks
|
||||
* are presented in the DOM
|
||||
* @param {DomBinding} [binding] You should not set this property. This is
|
||||
* used if DomBinding wants to create a
|
||||
@@ -63,13 +70,13 @@ export default class YXmlHook extends YMap {
|
||||
* This is called when data is received from a remote peer.
|
||||
*
|
||||
* @param {Y} y The Yjs instance that this Item belongs to.
|
||||
* @param {BinaryDecoder} decoder The decoder object to read data from.
|
||||
* @param {decoding.Decoder} decoder The decoder object to read data from.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_fromBinary (y, decoder) {
|
||||
const missing = super._fromBinary(y, decoder)
|
||||
this.hookName = decoder.readVarString()
|
||||
this.hookName = decoding.readVarString(decoder)
|
||||
return missing
|
||||
}
|
||||
|
||||
@@ -79,13 +86,13 @@ export default class YXmlHook extends YMap {
|
||||
*
|
||||
* This is called when this Item is sent to a remote peer.
|
||||
*
|
||||
* @param {BinaryEncoder} encoder The encoder to write data to.
|
||||
* @param {encoding.Encoder} encoder The encoder to write data to.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_toBinary (encoder) {
|
||||
super._toBinary(encoder)
|
||||
encoder.writeVarString(this.hookName)
|
||||
encoding.writeVarString(encoder, this.hookName)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1,5 +1,10 @@
|
||||
import YText from '../YText/YText.mjs'
|
||||
import { createAssociation } from '../../Bindings/DomBinding/util.mjs'
|
||||
import YText from '../YText/YText.js'
|
||||
import { createAssociation } from '../../Bindings/DomBinding/util.js'
|
||||
|
||||
/**
|
||||
* @typedef {import('../../Bindings/DomBinding/DomBinding.js').default} DomBinding
|
||||
* @typedef {import('../../index.js').Y} Y
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents text in a Dom Element. In the future this type will also handle
|
||||
@@ -14,12 +19,12 @@ export default class YXmlText extends YText {
|
||||
* @param {Document} [_document=document] The document object (you must define
|
||||
* this when calling this method in
|
||||
* nodejs)
|
||||
* @param {Object<key:hookDefinition>} [hooks] Optional property to customize how hooks
|
||||
* @param {Object<string, any>} [hooks] Optional property to customize how hooks
|
||||
* are presented in the DOM
|
||||
* @param {DomBinding} [binding] You should not set this property. This is
|
||||
* used if DomBinding wants to create a
|
||||
* association to the created DOM type.
|
||||
* @return {Element} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}
|
||||
* @return {Text} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
@@ -1,4 +1,4 @@
|
||||
import YXmlFragment from './YXmlFragment.mjs'
|
||||
import YXmlFragment from './YXmlFragment.js'
|
||||
|
||||
/**
|
||||
* Define the elements to which a set of CSS queries apply.
|
||||
@@ -33,7 +33,7 @@ export default class YXmlTreeWalker {
|
||||
/**
|
||||
* Get the next node.
|
||||
*
|
||||
* @return {YXmlElement} The next node.
|
||||
* @return {import('./YXmlElement.js').default} The next node.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
@@ -1,187 +0,0 @@
|
||||
import ID from '../ID/ID.mjs'
|
||||
import { default as RootID, RootFakeUserID } from '../ID/RootID.mjs'
|
||||
|
||||
/**
|
||||
* A BinaryDecoder handles the decoding of an ArrayBuffer.
|
||||
*/
|
||||
export default class BinaryDecoder {
|
||||
/**
|
||||
* @param {Uint8Array|Buffer} buffer The binary data that this instance
|
||||
* decodes.
|
||||
*/
|
||||
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
|
||||
}
|
||||
|
||||
hasContent () {
|
||||
return this.pos !== this.uint8arr.length
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Read `len` bytes as an ArrayBuffer.
|
||||
*/
|
||||
readArrayBuffer (len) {
|
||||
const arrayBuffer = new Uint8Array(len)
|
||||
const view = new Uint8Array(this.uint8arr.buffer, this.pos, len)
|
||||
arrayBuffer.set(view)
|
||||
this.pos += len
|
||||
return arrayBuffer.buffer
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @return {number} An 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.
|
||||
*
|
||||
* @return {number} An 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.
|
||||
*
|
||||
* @return {number} An unsigned integer.
|
||||
*/
|
||||
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
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* @return {String} The read String.
|
||||
*/
|
||||
readVarString () {
|
||||
let remainingLen = this.readVarUint()
|
||||
let encodedString = ''
|
||||
let i = 0
|
||||
while (remainingLen > 0) {
|
||||
const nextLen = Math.min(remainingLen, 10000)
|
||||
const bytes = new Array(nextLen)
|
||||
for (let i = 0; i < nextLen; i++) {
|
||||
bytes[i] = this.uint8arr[this.pos++]
|
||||
}
|
||||
encodedString += String.fromCodePoint.apply(null, bytes)
|
||||
remainingLen -= nextLen
|
||||
}
|
||||
/*
|
||||
//let bytes = new Array(len)
|
||||
for (let i = 0; i < len; i++) {
|
||||
//bytes[i] = this.uint8arr[this.pos++]
|
||||
encodedString += String.fromCodePoint(this.uint8arr[this.pos++])
|
||||
// encodedString += String(this.uint8arr[this.pos++])
|
||||
}
|
||||
*/
|
||||
//let encodedString = String.fromCodePoint.apply(null, bytes)
|
||||
return decodeURIComponent(escape(encodedString))
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @return ID
|
||||
*/
|
||||
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())
|
||||
}
|
||||
}
|
||||
@@ -1,210 +0,0 @@
|
||||
import { RootFakeUserID } from '../ID/RootID.mjs'
|
||||
|
||||
const bits7 = 0b1111111
|
||||
const bits8 = 0b11111111
|
||||
|
||||
/**
|
||||
* A BinaryEncoder handles the encoding to an ArrayBuffer.
|
||||
*/
|
||||
export default class BinaryEncoder {
|
||||
constructor () {
|
||||
// TODO: implement chained Uint8Array buffers instead of Array buffer
|
||||
// TODO: Rewrite all methods as functions!
|
||||
this._currentPos = 0
|
||||
this._currentBuffer = new Uint8Array(1000)
|
||||
this._data = []
|
||||
}
|
||||
|
||||
/**
|
||||
* The current length of the encoded data.
|
||||
*/
|
||||
get length () {
|
||||
let len = 0
|
||||
for (let i = 0; i < this._data.length; i++) {
|
||||
len += this._data[i].length
|
||||
}
|
||||
len += this._currentPos
|
||||
return len
|
||||
}
|
||||
|
||||
/**
|
||||
* The current write pointer (the same as {@link length}).
|
||||
*/
|
||||
get pos () {
|
||||
return this.length
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform to ArrayBuffer.
|
||||
*
|
||||
* @return {ArrayBuffer} The created ArrayBuffer.
|
||||
*/
|
||||
createBuffer () {
|
||||
const len = this.length
|
||||
const uint8array = new Uint8Array(len)
|
||||
let curPos = 0
|
||||
for (let i = 0; i < this._data.length; i++) {
|
||||
let d = this._data[i]
|
||||
uint8array.set(d, curPos)
|
||||
curPos += d.length
|
||||
}
|
||||
uint8array.set(new Uint8Array(this._currentBuffer.buffer, 0, this._currentPos), curPos)
|
||||
return uint8array.buffer
|
||||
}
|
||||
|
||||
/**
|
||||
* Write one byte to the encoder.
|
||||
*
|
||||
* @param {number} num The byte that is to be encoded.
|
||||
*/
|
||||
write (num) {
|
||||
if (this._currentPos === this._currentBuffer.length) {
|
||||
this._data.push(this._currentBuffer)
|
||||
this._currentBuffer = new Uint8Array(this._currentBuffer.length * 2)
|
||||
this._currentPos = 0
|
||||
}
|
||||
this._currentBuffer[this._currentPos++] = num
|
||||
}
|
||||
|
||||
set (pos, num) {
|
||||
let buffer = null
|
||||
// iterate all buffers and adjust position
|
||||
for (let i = 0; i < this._data.length && buffer === null; i++) {
|
||||
const b = this._data[i]
|
||||
if (pos < b.length) {
|
||||
buffer = b // found buffer
|
||||
} else {
|
||||
pos -= b.length
|
||||
}
|
||||
}
|
||||
if (buffer === null) {
|
||||
// use current buffer
|
||||
buffer = this._currentBuffer
|
||||
}
|
||||
buffer[pos] = num
|
||||
}
|
||||
|
||||
/**
|
||||
* Write one byte as an unsigned integer.
|
||||
*
|
||||
* @param {number} num The number that is to be encoded.
|
||||
*/
|
||||
writeUint8 (num) {
|
||||
this.write(num & bits8)
|
||||
}
|
||||
|
||||
/**
|
||||
* Write one byte as an unsigned Integer at a specific location.
|
||||
*
|
||||
* @param {number} pos The location where the data will be written.
|
||||
* @param {number} num The number that is to be encoded.
|
||||
*/
|
||||
setUint8 (pos, num) {
|
||||
this.set(pos, num & bits8)
|
||||
}
|
||||
|
||||
/**
|
||||
* Write two bytes as an unsigned integer.
|
||||
*
|
||||
* @param {number} num The number that is to be encoded.
|
||||
*/
|
||||
writeUint16 (num) {
|
||||
this.write(num & bits8)
|
||||
this.write((num >>> 8) & bits8)
|
||||
}
|
||||
/**
|
||||
* Write two bytes as an unsigned integer at a specific location.
|
||||
*
|
||||
* @param {number} pos The location where the data will be written.
|
||||
* @param {number} num The number that is to be encoded.
|
||||
*/
|
||||
setUint16 (pos, num) {
|
||||
this.set(pos, num & bits8)
|
||||
this.set(pos + 1, (num >>> 8) & bits8)
|
||||
}
|
||||
|
||||
/**
|
||||
* Write two bytes as an unsigned integer
|
||||
*
|
||||
* @param {number} num The number that is to be encoded.
|
||||
*/
|
||||
writeUint32 (num) {
|
||||
for (let i = 0; i < 4; i++) {
|
||||
this.write(num & bits8)
|
||||
num >>>= 8
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write two bytes as an unsigned integer at a specific location.
|
||||
*
|
||||
* @param {number} pos The location where the data will be written.
|
||||
* @param {number} num The number that is to be encoded.
|
||||
*/
|
||||
setUint32 (pos, num) {
|
||||
for (let i = 0; i < 4; i++) {
|
||||
this.set(pos + i, num & bits8)
|
||||
num >>>= 8
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a variable length unsigned integer.
|
||||
*
|
||||
* @param {number} num The number that is to be encoded.
|
||||
*/
|
||||
writeVarUint (num) {
|
||||
while (num >= 0b10000000) {
|
||||
this.write(0b10000000 | (bits7 & num))
|
||||
num >>>= 7
|
||||
}
|
||||
this.write(bits7 & num)
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a variable length string.
|
||||
*
|
||||
* @param {String} str The string that is to be encoded.
|
||||
*/
|
||||
writeVarString (str) {
|
||||
const encodedString = unescape(encodeURIComponent(str))
|
||||
const len = encodedString.length
|
||||
this.writeVarUint(len)
|
||||
for (let i = 0; i < len; i++) {
|
||||
this.write(encodedString.codePointAt(i))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the content of another binary encoder.
|
||||
*
|
||||
* @param encoder The BinaryEncoder to be written.
|
||||
*/
|
||||
writeBinaryEncoder (encoder) {
|
||||
this.writeArrayBuffer(encoder.createBuffer())
|
||||
}
|
||||
|
||||
writeArrayBuffer (arrayBuffer) {
|
||||
const prevBufferLen = this._currentBuffer.length
|
||||
this._data.push(new Uint8Array(this._currentBuffer.buffer, 0, this._currentPos))
|
||||
this._data.push(new Uint8Array(arrayBuffer))
|
||||
this._currentBuffer = new Uint8Array(prevBufferLen)
|
||||
this._currentPos = 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Write an ID at the current position.
|
||||
*
|
||||
* @param {ID} id The ID that is to be written.
|
||||
*/
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
90
src/Util/ID.js
Normal file
90
src/Util/ID.js
Normal file
@@ -0,0 +1,90 @@
|
||||
import { getStructReference } from './structReferences.js'
|
||||
import * as decoding from '../../lib/decoding.js'
|
||||
import * as encoding from '../../lib/encoding.js'
|
||||
|
||||
export class ID {
|
||||
constructor (user, clock) {
|
||||
this.user = user // TODO: rename to client
|
||||
this.clock = clock
|
||||
}
|
||||
clone () {
|
||||
return new ID(this.user, this.clock)
|
||||
}
|
||||
equals (id) {
|
||||
return id !== null && id.user === this.user && id.clock === this.clock
|
||||
}
|
||||
lessThan (id) {
|
||||
if (id.constructor === ID) {
|
||||
return this.user < id.user || (this.user === id.user && this.clock < id.clock)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @param {encoding.Encoder} encoder
|
||||
*/
|
||||
encode (encoder) {
|
||||
encoding.writeVarUint(encoder, this.user)
|
||||
encoding.writeVarUint(encoder, this.clock)
|
||||
}
|
||||
}
|
||||
|
||||
export const createID = (user, clock) => new ID(user, clock)
|
||||
|
||||
export const RootFakeUserID = 0xFFFFFF
|
||||
|
||||
export class RootID {
|
||||
constructor (name, typeConstructor) {
|
||||
this.user = RootFakeUserID
|
||||
this.name = name
|
||||
this.type = getStructReference(typeConstructor)
|
||||
}
|
||||
equals (id) {
|
||||
return id !== null && id.user === this.user && id.name === this.name && id.type === this.type
|
||||
}
|
||||
lessThan (id) {
|
||||
if (id.constructor === RootID) {
|
||||
return this.user < id.user || (this.user === id.user && (this.name < id.name || (this.name === id.name && this.type < id.type)))
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @param {encoding.Encoder} encoder
|
||||
*/
|
||||
encode (encoder) {
|
||||
encoding.writeVarUint(encoder, this.user)
|
||||
encoding.writeVarString(encoder, this.name)
|
||||
encoding.writeVarUint(encoder, this.type)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new root id.
|
||||
*
|
||||
* @example
|
||||
* y.define('name', Y.Array) // name, and typeConstructor
|
||||
*
|
||||
* @param {string} name
|
||||
* @param {Function} typeConstructor must be defined in structReferences
|
||||
*/
|
||||
export const createRootID = (name, typeConstructor) => new RootID(name, typeConstructor)
|
||||
|
||||
/**
|
||||
* Read ID.
|
||||
* * If first varUint read is 0xFFFFFF a RootID is returned.
|
||||
* * Otherwise an ID is returned
|
||||
*
|
||||
* @param {decoding.Decoder} decoder
|
||||
* @return {ID|RootID}
|
||||
*/
|
||||
export const decode = decoder => {
|
||||
const user = decoding.readVarUint(decoder)
|
||||
if (user === RootFakeUserID) {
|
||||
// read property name and type id
|
||||
const rid = createRootID(decoding.readVarString(decoder), null)
|
||||
rid.type = decoding.readVarUint(decoder)
|
||||
return rid
|
||||
}
|
||||
return createID(user, decoding.readVarUint(decoder))
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
|
||||
export default class ID {
|
||||
constructor (user, clock) {
|
||||
this.user = user // TODO: rename to client
|
||||
this.clock = clock
|
||||
}
|
||||
clone () {
|
||||
return new ID(this.user, this.clock)
|
||||
}
|
||||
equals (id) {
|
||||
return id !== null && id.user === this.user && id.clock === this.clock
|
||||
}
|
||||
lessThan (id) {
|
||||
if (id.constructor === ID) {
|
||||
return this.user < id.user || (this.user === id.user && this.clock < id.clock)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user