start refactoring
This commit is contained in:
parent
4c01a34d09
commit
dece14486c
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,10 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<!--script type="module" src="./encoding.test.js"></script-->
|
|
||||||
<script type="module" src="./idb.test.js"></script>
|
|
||||||
<script type="module" src="./ydb-client.test.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -12,7 +12,7 @@
|
|||||||
*
|
*
|
||||||
* @typedef {Object} SimpleDiff
|
* @typedef {Object} SimpleDiff
|
||||||
* @property {Number} pos The index where changes were applied
|
* @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`.
|
* at `index`.
|
||||||
* @property {String} insert The new text to insert at `index` after applying
|
* @property {String} insert The new text to insert at `index` after applying
|
||||||
* `delete`
|
* `delete`
|
@ -1,4 +1,5 @@
|
|||||||
import * as logging from './logging.js'
|
import * as logging from './logging.js'
|
||||||
|
import simpleDiff from './simpleDiff.js'
|
||||||
|
|
||||||
export const run = async (name, f) => {
|
export const run = async (name, f) => {
|
||||||
console.log(`%cStart:%c ${name}`, 'color:blue;', '')
|
console.log(`%cStart:%c ${name}`, 'color:blue;', '')
|
||||||
@ -23,3 +24,10 @@ export const compareArrays = (as, bs) => {
|
|||||||
}
|
}
|
||||||
return true
|
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')
|
||||||
|
}
|
||||||
|
}
|
2218
package-lock.json
generated
2218
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
import { createMutualExclude } from '../Util/mutualExclude.js'
|
import { createMutualExclude } from '../../lib/mutualExclude.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract class for bindings.
|
* Abstract class for bindings.
|
||||||
|
@ -4,7 +4,7 @@ import {
|
|||||||
iterateUntilUndeleted,
|
iterateUntilUndeleted,
|
||||||
removeAssociation,
|
removeAssociation,
|
||||||
insertNodeHelper } from './util.js'
|
insertNodeHelper } from './util.js'
|
||||||
import diff from '../../Util/simpleDiff.js'
|
import diff from '../../../lib/simpleDiff.js'
|
||||||
import YXmlFragment from '../../Types/YXml/YXmlFragment.js'
|
import YXmlFragment from '../../Types/YXml/YXmlFragment.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
import Binding from '../Binding.js'
|
import Binding from '../Binding.js'
|
||||||
import simpleDiff from '../../Util/simpleDiff.js'
|
import simpleDiff from '../../../lib/simpleDiff.js'
|
||||||
import { getRelativePosition, fromRelativePosition } from '../../Util/relativePosition.js'
|
import { getRelativePosition, fromRelativePosition } from '../../Util/relativePosition.js'
|
||||||
|
|
||||||
function typeObserver () {
|
function typeObserver () {
|
||||||
|
298
src/Connector.js
298
src/Connector.js
@ -1,298 +0,0 @@
|
|||||||
import BinaryEncoder from './Util/Binary/Encoder.js'
|
|
||||||
import BinaryDecoder from './Util/Binary/Decoder.js'
|
|
||||||
|
|
||||||
import { sendSyncStep1, readSyncStep1 } from './MessageHandler/syncStep1.js'
|
|
||||||
import { readSyncStep2 } from './MessageHandler/syncStep2.js'
|
|
||||||
import { integrateRemoteStructs } from './MessageHandler/integrateRemoteStructs.js'
|
|
||||||
|
|
||||||
// TODO: reintroduce or remove
|
|
||||||
// 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.js'
|
|
||||||
/* global WebSocket */
|
|
||||||
import NamedEventHandler from '../../Util/NamedEventHandler.js'
|
|
||||||
import decodeMessage, { messageSS, messageSubscribe, messageStructs } from './decodeMessage.js'
|
|
||||||
import { createMutualExclude } from '../../Util/mutualExclude.js'
|
|
||||||
import { messageCheckUpdateCounter } from './decodeMessage.js'
|
|
||||||
|
|
||||||
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.js'
|
|
||||||
import BinaryEncoder from '../../Util/Binary/Encoder.js'
|
|
||||||
import { readStateSet, writeStateSet } from '../../MessageHandler/stateSet.js'
|
|
||||||
import { writeStructs } from '../../MessageHandler/syncStep1.js'
|
|
||||||
import { writeDeleteSet, readDeleteSet } from '../../MessageHandler/deleteSet.js'
|
|
||||||
import { integrateRemoteStructs } from '../../MessageHandler/integrateRemoteStructs.js'
|
|
||||||
|
|
||||||
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.js'
|
|
||||||
import uws from 'uws'
|
|
||||||
import BinaryEncoder from '../../Util/Binary/Encoder.js'
|
|
||||||
import decodeMessage, { messageStructs } from './decodeMessage.js'
|
|
||||||
import FilePersistence from '../../Persistences/FilePersistence.js'
|
|
||||||
|
|
||||||
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,122 +0,0 @@
|
|||||||
import BinaryEncoder from './Util/Binary/Encoder.js'
|
|
||||||
import BinaryDecoder from './Util/Binary/Decoder.js'
|
|
||||||
import { toBinary, fromBinary } from './MessageHandler/binaryEncode.js'
|
|
||||||
import { integrateRemoteStructs } from './MessageHandler/integrateRemoteStructs.js'
|
|
||||||
import { createMutualExclude } from './Util/mutualExclude.js'
|
|
||||||
|
|
||||||
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,5 +1,5 @@
|
|||||||
|
|
||||||
import Tree from '../Util/Tree.js'
|
import Tree from '../../lib/Tree.js'
|
||||||
import ID from '../Util/ID/ID.js'
|
import ID from '../Util/ID/ID.js'
|
||||||
|
|
||||||
class DSNode {
|
class DSNode {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import Tree from '../Util/Tree.js'
|
import Tree from '../../lib/Tree.js'
|
||||||
import RootID from '../Util/ID/RootID.js'
|
import RootID from '../Util/ID/RootID.js'
|
||||||
import { getStruct } from '../Util/structReferences.js'
|
import { getStruct } from '../Util/structReferences.js'
|
||||||
import { logID } from '../MessageHandler/messageToString.js'
|
import { logID } from '../MessageHandler/messageToString.js'
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { getStructReference } from '../Util/structReferences.js'
|
import { getStructReference } from '../Util/structReferences.js'
|
||||||
import ID from '../Util/ID/ID.js'
|
import ID from '../Util/ID/ID.js'
|
||||||
import { logID } from '../MessageHandler/messageToString.js'
|
import { logID } from '../MessageHandler/messageToString.js'
|
||||||
import { writeStructToTransaction } from '../Transaction.js'
|
import { writeStructToTransaction } from '../Util/Transaction.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { getStructReference } from '../Util/structReferences.js'
|
import { getStructReference } from '../Util/structReferences.js'
|
||||||
import { RootFakeUserID } from '../Util/ID/RootID.js'
|
import { RootFakeUserID } from '../Util/ID/RootID.js'
|
||||||
import ID from '../Util/ID/ID.js'
|
import ID from '../Util/ID/ID.js'
|
||||||
import { writeStructToTransaction } from '../Transaction.js'
|
import { writeStructToTransaction } from '../Util/Transaction.js'
|
||||||
|
|
||||||
// TODO should have the same base class as Item
|
// TODO should have the same base class as Item
|
||||||
export default class GC {
|
export default class GC {
|
||||||
|
@ -2,7 +2,7 @@ import { getStructReference } from '../Util/structReferences.js'
|
|||||||
import ID from '../Util/ID/ID.js'
|
import ID from '../Util/ID/ID.js'
|
||||||
import { default as RootID, RootFakeUserID } from '../Util/ID/RootID.js'
|
import { default as RootID, RootFakeUserID } from '../Util/ID/RootID.js'
|
||||||
import Delete from './Delete.js'
|
import Delete from './Delete.js'
|
||||||
import { transactionTypeChanged, writeStructToTransaction } from '../Transaction.js'
|
import { transactionTypeChanged, writeStructToTransaction } from '../Util/Transaction.js'
|
||||||
import GC from './GC.js'
|
import GC from './GC.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,187 +0,0 @@
|
|||||||
import ID from '../ID/ID.js'
|
|
||||||
import { default as RootID, RootFakeUserID } from '../ID/RootID.js'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.js'
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,4 +1,4 @@
|
|||||||
import BinaryEncoder from './Util/Binary/Encoder.js'
|
import BinaryEncoder from './Binary/Encoder.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A transaction is created for every change on the Yjs model. It is possible
|
* A transaction is created for every change on the Yjs model. It is possible
|
@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
import Y from './Y.js'
|
export { default as Y } from './Y.js'
|
||||||
import UndoManager from './Util/UndoManager.js'
|
export { default as UndoManager } from './Util/UndoManager.js'
|
||||||
import { integrateRemoteStructs } from './MessageHandler/integrateRemoteStructs.js'
|
export { integrateRemoteStructs } from './MessageHandler/integrateRemoteStructs.js'
|
||||||
|
|
||||||
import Connector from './Connector.js'
|
import Connector from './Connector.js'
|
||||||
import Persistence from './Persistence.js'
|
import Persistence from './Persistence.js'
|
||||||
@ -52,5 +52,3 @@ Y.utils = {
|
|||||||
toBinary,
|
toBinary,
|
||||||
fromBinary
|
fromBinary
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Y
|
|
||||||
|
4
src/Y.js
4
src/Y.js
@ -3,8 +3,8 @@ import OperationStore from './Store/OperationStore.js'
|
|||||||
import StateStore from './Store/StateStore.js'
|
import StateStore from './Store/StateStore.js'
|
||||||
import { generateRandomUint32 } from './Util/generateRandomUint32.js'
|
import { generateRandomUint32 } from './Util/generateRandomUint32.js'
|
||||||
import RootID from './Util/ID/RootID.js'
|
import RootID from './Util/ID/RootID.js'
|
||||||
import NamedEventHandler from './Util/NamedEventHandler.js'
|
import NamedEventHandler from '../lib/NamedEventHandler.js'
|
||||||
import Transaction from './Transaction.js'
|
import Transaction from './Util/Transaction.js'
|
||||||
|
|
||||||
export { default as DomBinding } from './Bindings/DomBinding/DomBinding.js'
|
export { default as DomBinding } from './Bindings/DomBinding/DomBinding.js'
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { test } from '../node_modules/cutest/cutest.js'
|
import { test } from '../node_modules/cutest/cutest.js'
|
||||||
import simpleDiff from '../src/Util/simpleDiff.js'
|
import simpleDiff from '../lib/simpleDiff.js'
|
||||||
import Chance from 'chance'
|
import Chance from 'chance'
|
||||||
|
|
||||||
function runDiffTest (t, a, b, expected) {
|
function runDiffTest (t, a, b, expected) {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import RedBlackTree from '../src/Util/Tree.js'
|
import RedBlackTree from '../lib/Tree.js'
|
||||||
import ID from '../src/Util/ID/ID.js'
|
import ID from '../src/Util/ID/ID.js'
|
||||||
import Chance from 'chance'
|
import Chance from 'chance'
|
||||||
import { test, proxyConsole } from 'cutest'
|
import { test, proxyConsole } from 'cutest'
|
||||||
|
Loading…
x
Reference in New Issue
Block a user