start refactoring

This commit is contained in:
Kevin Jahns 2018-10-22 12:23:35 +02:00
parent 4c01a34d09
commit dece14486c
46 changed files with 1127 additions and 2411 deletions

View File

@ -1,12 +0,0 @@
{
"presets": [
["latest", {
"es2015": {
"modules": false
}
}]
],
"plugins": [
"external-helpers"
]
}

View File

@ -1,14 +0,0 @@
[ignore]
.*/node_modules/.*
.*/dist/.*
.*/build/.*
[include]
./src/
./tests-lib/
./test/
[libs]
./declarations/
[options]

View File

@ -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>

View File

@ -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`

View File

@ -1,4 +1,5 @@
import * as logging from './logging.js'
import simpleDiff from './simpleDiff.js'
export const run = async (name, f) => {
console.log(`%cStart:%c ${name}`, 'color:blue;', '')
@ -23,3 +24,10 @@ export const compareArrays = (as, bs) => {
}
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

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
import { createMutualExclude } from '../Util/mutualExclude.js'
import { createMutualExclude } from '../../lib/mutualExclude.js'
/**
* Abstract class for bindings.

View File

@ -4,7 +4,7 @@ import {
iterateUntilUndeleted,
removeAssociation,
insertNodeHelper } from './util.js'
import diff from '../../Util/simpleDiff.js'
import diff from '../../../lib/simpleDiff.js'
import YXmlFragment from '../../Types/YXml/YXmlFragment.js'
/**

View File

@ -1,6 +1,6 @@
import Binding from '../Binding.js'
import simpleDiff from '../../Util/simpleDiff.js'
import simpleDiff from '../../../lib/simpleDiff.js'
import { getRelativePosition, fromRelativePosition } from '../../Util/relativePosition.js'
function typeObserver () {

View File

@ -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()
}
}
}

View File

@ -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))
}
}
}

View File

@ -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
}

View File

@ -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)
}
})
})

View File

@ -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()
}
}

View File

@ -1,5 +1,5 @@
import Tree from '../Util/Tree.js'
import Tree from '../../lib/Tree.js'
import ID from '../Util/ID/ID.js'
class DSNode {

View File

@ -1,4 +1,4 @@
import Tree from '../Util/Tree.js'
import Tree from '../../lib/Tree.js'
import RootID from '../Util/ID/RootID.js'
import { getStruct } from '../Util/structReferences.js'
import { logID } from '../MessageHandler/messageToString.js'

View File

@ -1,7 +1,7 @@
import { getStructReference } from '../Util/structReferences.js'
import ID from '../Util/ID/ID.js'
import { logID } from '../MessageHandler/messageToString.js'
import { writeStructToTransaction } from '../Transaction.js'
import { writeStructToTransaction } from '../Util/Transaction.js'
/**
* @private

View File

@ -1,7 +1,7 @@
import { getStructReference } from '../Util/structReferences.js'
import { RootFakeUserID } from '../Util/ID/RootID.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
export default class GC {

View File

@ -2,7 +2,7 @@ import { getStructReference } from '../Util/structReferences.js'
import ID from '../Util/ID/ID.js'
import { default as RootID, RootFakeUserID } from '../Util/ID/RootID.js'
import Delete from './Delete.js'
import { transactionTypeChanged, writeStructToTransaction } from '../Transaction.js'
import { transactionTypeChanged, writeStructToTransaction } from '../Util/Transaction.js'
import GC from './GC.js'
/**

View File

@ -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())
}
}

View File

@ -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)
}
}
}

View File

@ -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

View File

@ -1,7 +1,7 @@
import Y from './Y.js'
import UndoManager from './Util/UndoManager.js'
import { integrateRemoteStructs } from './MessageHandler/integrateRemoteStructs.js'
export { default as Y } from './Y.js'
export { default as UndoManager } from './Util/UndoManager.js'
export { integrateRemoteStructs } from './MessageHandler/integrateRemoteStructs.js'
import Connector from './Connector.js'
import Persistence from './Persistence.js'
@ -52,5 +52,3 @@ Y.utils = {
toBinary,
fromBinary
}
export default Y

View File

@ -3,8 +3,8 @@ import OperationStore from './Store/OperationStore.js'
import StateStore from './Store/StateStore.js'
import { generateRandomUint32 } from './Util/generateRandomUint32.js'
import RootID from './Util/ID/RootID.js'
import NamedEventHandler from './Util/NamedEventHandler.js'
import Transaction from './Transaction.js'
import NamedEventHandler from '../lib/NamedEventHandler.js'
import Transaction from './Util/Transaction.js'
export { default as DomBinding } from './Bindings/DomBinding/DomBinding.js'

View File

@ -1,5 +1,5 @@
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'
function runDiffTest (t, a, b, expected) {

View File

@ -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 Chance from 'chance'
import { test, proxyConsole } from 'cutest'