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': [
|
'/norm-editor/.websocket': [
|
||||||
'controller::socket:NormEditor',
|
'controller::socket:NormEditor',
|
||||||
],
|
],
|
||||||
|
'/editor/.websocket': [
|
||||||
|
'controller::socket:Editor',
|
||||||
|
],
|
||||||
|
'/code/.websocket': [
|
||||||
|
'controller::socket:Code',
|
||||||
|
],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user