Start generic websocket framework for real-time editor
This commit is contained in:
parent
572edda4ae
commit
0e4160a752
10
app/controllers/socket/Code.controller.js
Normal file
10
app/controllers/socket/Code.controller.js
Normal file
@ -0,0 +1,10 @@
|
||||
const EditorController = require('./Editor.controller')
|
||||
|
||||
class CodeController extends EditorController {
|
||||
async _get_resource(id) {
|
||||
const Codium = this.models.get('api:Codium')
|
||||
return Codium.findOne({ UUID: id })
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = CodeController
|
241
app/controllers/socket/Editor.controller.js
Normal file
241
app/controllers/socket/Editor.controller.js
Normal file
@ -0,0 +1,241 @@
|
||||
const SocketController = require('flitter-socket/Controller')
|
||||
const uuid = require('uuid').v4
|
||||
|
||||
function getColor(){
|
||||
return "hsl(" + 360 * Math.random() + ',' +
|
||||
(25 + 70 * Math.random()) + '%,' +
|
||||
(85 + 10 * Math.random()) + '%)'
|
||||
}
|
||||
|
||||
class EditorGroup {
|
||||
constructor(resource_id, resource) {
|
||||
this.id = resource_id
|
||||
this.resource = resource
|
||||
this.connections = {}
|
||||
this.connection_id_x_session = {}
|
||||
}
|
||||
|
||||
join(connection_manager) {
|
||||
this.connections[connection_manager.id] = connection_manager
|
||||
this.connection_id_x_session[connection_manager.id] = {}
|
||||
|
||||
connection_manager.on_close().then(() => {
|
||||
delete this.connection_id_x_session[connection_manager.id]
|
||||
delete this.connections[connection_manager.id]
|
||||
this.blast_editor_group_users()
|
||||
})
|
||||
}
|
||||
|
||||
has(connection_manager) {
|
||||
return !!this.connections[connection_manager.id]
|
||||
}
|
||||
|
||||
unique_users() {
|
||||
const users = {}
|
||||
const conns = Object.values(this.connections)
|
||||
|
||||
for ( const conn of conns ) {
|
||||
const session = this.session(conn)
|
||||
const user = conn.request.user
|
||||
if ( user && !users[user.uuid] ) {
|
||||
if ( !session.user_color ) {
|
||||
session.user_color = getColor() // '#' + Math.floor(Math.random() * 16777215).toString(16)
|
||||
while ( session.user_color.length < 7 ) {
|
||||
session.user_color += '5'
|
||||
}
|
||||
}
|
||||
|
||||
users[user.uuid] = { user, color: session.user_color }
|
||||
}
|
||||
}
|
||||
|
||||
return Object.values(users)
|
||||
}
|
||||
|
||||
session(conn) {
|
||||
return this.connection_id_x_session[conn.id]
|
||||
}
|
||||
|
||||
blast_editor_group_users() {
|
||||
const conns = Object.values(this.connections)
|
||||
const unique_users = this.unique_users()
|
||||
console.log('blast_editor_group_users', unique_users)
|
||||
|
||||
for ( const conn of conns ) {
|
||||
const users = unique_users.filter(data => data.user.uuid !== conn.request.user.uuid)
|
||||
.map(data => {
|
||||
const { user, color } = data
|
||||
return {
|
||||
uuid: user.uuid,
|
||||
uid: user.uid,
|
||||
display: user.uid,
|
||||
color,
|
||||
}
|
||||
})
|
||||
|
||||
conn._request('setEditorGroupUsers', { users })
|
||||
}
|
||||
}
|
||||
|
||||
blast_operations(operations, exclude_connection_id) {
|
||||
const conns = Object.values(this.connections)
|
||||
|
||||
for ( const conn of conns ) {
|
||||
if ( conn.id === exclude_connection_id ) continue;
|
||||
|
||||
conn._request('applyRemoteOperation', { operations })
|
||||
}
|
||||
}
|
||||
|
||||
blast_position(position, uuid, exclude_connection_id) {
|
||||
const conns = Object.values(this.connections)
|
||||
|
||||
for ( const conn of conns ) {
|
||||
if ( conn.id === exclude_connection_id ) continue;
|
||||
|
||||
conn._request('updateCursorPosition', { position, uuid })
|
||||
}
|
||||
}
|
||||
|
||||
blast_selection(startPosition, endPosition, uuid, exclude_connection_id) {
|
||||
const conns = Object.values(this.connections)
|
||||
|
||||
for ( const conn of conns ) {
|
||||
if ( conn.id === exclude_connection_id ) continue;
|
||||
|
||||
conn._request('updateSelection', { startPosition, endPosition, uuid })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class EditorController extends SocketController {
|
||||
editor_groups = {}
|
||||
|
||||
static get services() {
|
||||
return [...super.services, 'output', 'models']
|
||||
}
|
||||
|
||||
ping(transaction, socket) {
|
||||
this.output.info('Got norm editor socket ping.')
|
||||
|
||||
transaction.status(200)
|
||||
.message('Pong!')
|
||||
.send(transaction.incoming)
|
||||
}
|
||||
|
||||
async apply(transaction, socket) {
|
||||
const group = this._get_editor_group_or_fail(transaction)
|
||||
if ( !group ) return
|
||||
|
||||
// FIXME validate
|
||||
const operations = transaction.incoming.operations.map(x => typeof x === 'string' ? JSON.parse(x) : x)
|
||||
await group.blast_operations(operations, transaction.cm.id)
|
||||
|
||||
transaction.send()
|
||||
}
|
||||
|
||||
async update_cursor(transaction, socket) {
|
||||
const group = this._get_editor_group_or_fail(transaction)
|
||||
if ( !group ) return
|
||||
|
||||
// FIXME validate
|
||||
const position = transaction.incoming.position
|
||||
const uuid = transaction.incoming.uuid
|
||||
await group.blast_position(position, uuid, transaction.cm.id)
|
||||
|
||||
transaction.send()
|
||||
}
|
||||
|
||||
async update_selection(transaction, socket) {
|
||||
const group = this._get_editor_group_or_fail(transaction)
|
||||
if ( !group ) return
|
||||
|
||||
// FIXME validate
|
||||
const start = transaction.incoming.startPosition
|
||||
const end = transaction.incoming.endPosition
|
||||
const uuid = transaction.incoming.uuid
|
||||
await group.blast_selection(start, end, uuid, transaction.cm.id)
|
||||
|
||||
transaction.send()
|
||||
}
|
||||
|
||||
async subscribe(transaction, socket) {
|
||||
if ( !transaction.incoming.resource_id ) {
|
||||
return transaction.status(400)
|
||||
.message('Missing resource ID.')
|
||||
.send()
|
||||
}
|
||||
|
||||
const resource = await this._get_resource(transaction.incoming.resource_id)
|
||||
if ( !resource ) {
|
||||
return transaction.status(404)
|
||||
.message('Invalid resource ID.')
|
||||
.send()
|
||||
}
|
||||
|
||||
// Try to look up an existing editor group
|
||||
let editor_group = this.editor_groups[transaction.incoming.resource_id]
|
||||
|
||||
if ( !editor_group ) {
|
||||
// create a new editor group
|
||||
editor_group = new EditorGroup(transaction.incoming.resource_id, resource)
|
||||
this.editor_groups[transaction.incoming.resource_id] = editor_group
|
||||
}
|
||||
|
||||
if ( !editor_group.has(transaction.cm) )
|
||||
editor_group.join(transaction.cm)
|
||||
|
||||
transaction.status(200)
|
||||
.message('Joined editor group.')
|
||||
.send({
|
||||
editor_group_id: transaction.incoming.resource_id,
|
||||
local_user: {
|
||||
uid: transaction.cm.request.user.uid,
|
||||
uuid: transaction.cm.request.user.uuid,
|
||||
color: 'black',
|
||||
display: transaction.cm.request.user.uid,
|
||||
}
|
||||
})
|
||||
|
||||
editor_group.blast_editor_group_users()
|
||||
}
|
||||
|
||||
async _get_resource(id) {
|
||||
const Node = this.models.get('api:Node')
|
||||
return Node.findOne({ UUID: id })
|
||||
}
|
||||
|
||||
_get_editor_group_or_fail(transaction) {
|
||||
if ( !transaction.incoming.editor_group_id ) {
|
||||
transaction.status(400)
|
||||
.message('Missing editor_group_id.')
|
||||
.send()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const editor_group = this.editor_groups[transaction.incoming.editor_group_id]
|
||||
if ( !editor_group || !editor_group.has(transaction.cm) ) {
|
||||
transaction.status(400)
|
||||
.message('Invalid editor_group_id.')
|
||||
.send()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
return editor_group
|
||||
}
|
||||
|
||||
_client_connected(connection_manager) {
|
||||
super._client_connected(connection_manager)
|
||||
|
||||
this.output.debug('New client connected to editor socket.')
|
||||
|
||||
if ( !connection_manager.request?.session?.auth?.user_id ) {
|
||||
delete this.connections[connection_manager.id]
|
||||
connection_manager.socket.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = EditorController
|
@ -10,5 +10,11 @@ module.exports = exports = {
|
||||
'/norm-editor/.websocket': [
|
||||
'controller::socket:NormEditor',
|
||||
],
|
||||
'/editor/.websocket': [
|
||||
'controller::socket:Editor',
|
||||
],
|
||||
'/code/.websocket': [
|
||||
'controller::socket:Code',
|
||||
],
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user