hedgedoc/lib/ot/editor-socketio-server.ts

155 lines
4.5 KiB
TypeScript
Raw Normal View History

import { EventEmitter } from 'events'
import { logger } from '../logger'
import { SocketWithNoteId } from '../realtime'
import Selection from './selection'
import Server from './server'
import TextOperation from './text-operation'
import WrappedOperation from './wrapped-operation'
export class EditorSocketIOServer extends Server {
private readonly users: {}
private readonly docId: any
private mayWrite: (socket: SocketWithNoteId, originIsOperation: boolean, callback: (mayEdit: boolean) => void) => void
constructor (document, operations, docId, mayWrite, operationCallback) {
super(document, operations)
// Whatever that does?
EventEmitter.call(this)
this.users = {}
this.docId = docId
this.mayWrite = mayWrite || function (_, originIsOperation, cb) {
cb(true)
}
this.operationCallback = operationCallback
}
addClient (socket) {
const self = this
socket.join(this.docId)
const docOut = {
str: this.document,
revision: this.operations.length,
clients: this.users
}
socket.emit('doc', docOut)
socket.on('operation', function (revision, operation, selection) {
self.mayWrite(socket, true, function (mayWrite) {
if (!mayWrite) {
logger.info("User doesn't have the right to edit.")
return
}
try {
self.onOperation(socket, revision, operation, selection)
if (typeof self.operationCallback === 'function')
self.operationCallback(socket, operation)
} catch (err) {
setTimeout(function () {
const docOut = {
str: self.document,
revision: self.operations.length,
clients: self.users,
force: true
}
socket.emit('doc', docOut)
}, 100)
}
})
})
socket.on('get_operations', function (base, head) {
self.onGetOperations(socket, base, head)
})
socket.on('selection', function (obj) {
self.mayWrite(socket, false, function (mayWrite) {
if (!mayWrite) {
logger.info("User doesn't have the right to edit.")
return
}
self.updateSelection(socket, obj && Selection.fromJSON(obj))
})
})
socket.on('disconnect', function () {
logger.debug("Disconnect")
socket.leave(self.docId)
self.onDisconnect(socket)
/*
if (socket.manager && socket.manager.sockets.clients(self.docId).length === 0) {
self.emit('empty-room');
}
*/
})
};
onOperation (socket, revision, operation, selection) {
let wrapped
try {
wrapped = new WrappedOperation(
TextOperation.fromJSON(operation),
selection && Selection.fromJSON(selection)
)
} catch (exc) {
logger.error("Invalid operation received: ")
logger.error(exc)
throw new Error(exc)
}
try {
const clientId = socket.id
const wrappedPrime = this.receiveOperation(revision, wrapped)
if (!wrappedPrime) return
logger.debug("new operation: " + JSON.stringify(wrapped))
this.getClient(clientId).selection = wrappedPrime.meta
revision = this.operations.length
socket.emit('ack', revision)
socket.broadcast.in(this.docId).emit(
'operation', clientId, revision,
wrappedPrime.wrapped.toJSON(), wrappedPrime.meta
)
//set document is dirty
this.isDirty = true
} catch (exc) {
logger.error(exc)
throw new Error(exc)
}
};
onGetOperations (socket, base, head) {
const operations = this.operations.slice(base, head).map(function (op) {
return op.wrapped.toJSON()
})
socket.emit('operations', head, operations)
};
updateSelection (socket, selection) {
const clientId = socket.id
if (selection) {
this.getClient(clientId).selection = selection
} else {
delete this.getClient(clientId).selection
}
socket.broadcast.to(this.docId).emit('selection', clientId, selection)
};
setName (socket, name) {
const clientId = socket.id
this.getClient(clientId).name = name
socket.broadcast.to(this.docId).emit('set_name', clientId, name)
};
setColor (socket, color) {
const clientId = socket.id
this.getClient(clientId).color = color
socket.broadcast.to(this.docId).emit('set_color', clientId, color)
};
getClient (clientId) {
return this.users[clientId] || (this.users[clientId] = {})
};
onDisconnect (socket) {
const clientId = socket.id
delete this.users[clientId]
socket.broadcast.to(this.docId).emit('client_left', clientId)
};
}