Start generic websocket framework for real-time editor
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing

This commit is contained in:
Garrett Mills 2021-04-22 10:48:11 -05:00
parent 572edda4ae
commit 0e4160a752
Signed by: garrettmills
GPG Key ID: D2BF5FBA8298F246
3 changed files with 257 additions and 0 deletions

View 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

View 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

View File

@ -10,5 +10,11 @@ module.exports = exports = {
'/norm-editor/.websocket': [
'controller::socket:NormEditor',
],
'/editor/.websocket': [
'controller::socket:Editor',
],
'/code/.websocket': [
'controller::socket:Code',
],
}
}