191 lines
5.4 KiB
TypeScript
191 lines
5.4 KiB
TypeScript
import authorized from "../middleware/authorized"
|
|
import { BaseSocket, EmitOptions } from "./websocket"
|
|
import { permissions, events, context } from "@budibase/backend-core"
|
|
import http from "http"
|
|
import Koa from "koa"
|
|
import {
|
|
Datasource,
|
|
Table,
|
|
SocketSession,
|
|
ContextUser,
|
|
Screen,
|
|
App,
|
|
Automation,
|
|
} from "@budibase/types"
|
|
import { gridSocket } from "./index"
|
|
import { clearLock, updateLock } from "../utilities/redis"
|
|
import { Socket } from "socket.io"
|
|
import { BuilderSocketEvent } from "@budibase/shared-core"
|
|
|
|
export default class BuilderSocket extends BaseSocket {
|
|
constructor(app: Koa, server: http.Server) {
|
|
super(app, server, "/socket/builder", [authorized(permissions.BUILDER)])
|
|
}
|
|
|
|
async onConnect(socket?: Socket) {
|
|
// Initial identification of selected app
|
|
socket?.on(BuilderSocketEvent.SelectApp, async ({ appId }, callback) => {
|
|
await this.joinRoom(socket, appId)
|
|
const sessions = await this.getRoomSessions(appId)
|
|
|
|
// Track collaboration usage by unique users
|
|
let userIdMap: any = {}
|
|
sessions?.forEach(session => {
|
|
if (session._id) {
|
|
userIdMap[session._id] = true
|
|
}
|
|
})
|
|
|
|
const tenantId = context.getTenantIDFromAppID(appId)
|
|
if (tenantId) {
|
|
await context.doInTenant(tenantId, async () => {
|
|
await events.user.dataCollaboration(Object.keys(userIdMap).length)
|
|
})
|
|
}
|
|
|
|
// Reply with all current sessions
|
|
callback({ users: sessions })
|
|
})
|
|
|
|
// Handle users selecting a new cell
|
|
socket?.on(BuilderSocketEvent.SelectResource, ({ resourceId }) => {
|
|
this.updateUser(socket, { selectedResourceId: resourceId })
|
|
})
|
|
}
|
|
|
|
async onDisconnect(socket: Socket) {
|
|
// Remove app lock from this user if they have no other connections,
|
|
// and transfer it to someone else if possible
|
|
try {
|
|
// @ts-ignore
|
|
const session: SocketSession = socket.data
|
|
const { _id, sessionId, room } = session
|
|
const sessions = await this.getRoomSessions(room)
|
|
const hasOtherSession = sessions.some(otherSession => {
|
|
return _id === otherSession._id && sessionId !== otherSession.sessionId
|
|
})
|
|
if (!hasOtherSession && room) {
|
|
// Clear the lock from this user since they had no other sessions
|
|
// @ts-ignore
|
|
const user: ContextUser = { _id: socket.data._id }
|
|
await clearLock(room, user)
|
|
|
|
// Transfer lock ownership to the next oldest user
|
|
let otherSessions = sessions.filter(x => x._id !== _id).slice()
|
|
otherSessions.sort((a, b) => {
|
|
return a.connectedAt < b.connectedAt ? -1 : 1
|
|
})
|
|
const nextSession = otherSessions[0]
|
|
if (nextSession) {
|
|
const { _id, email, firstName, lastName } = nextSession
|
|
// @ts-ignore
|
|
const nextUser: ContextUser = { _id, email, firstName, lastName }
|
|
await updateLock(room, nextUser)
|
|
this.io.to(room).emit(BuilderSocketEvent.LockTransfer, {
|
|
userId: _id,
|
|
})
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// This is fine, just means this user didn't hold the lock
|
|
}
|
|
}
|
|
|
|
async updateUser(socket: Socket, patch: Object) {
|
|
await super.updateUser(socket, {
|
|
builderMetadata: {
|
|
...socket.data.builderMetadata,
|
|
...patch,
|
|
},
|
|
})
|
|
}
|
|
|
|
emitTableUpdate(ctx: any, table: Table, options?: EmitOptions) {
|
|
if (table.sourceId == null || table.sourceId === "") {
|
|
throw new Error("Table sourceId is not set")
|
|
}
|
|
|
|
this.emitToRoom(
|
|
ctx,
|
|
ctx.appId,
|
|
BuilderSocketEvent.TableChange,
|
|
{
|
|
id: table._id,
|
|
table,
|
|
},
|
|
options
|
|
)
|
|
gridSocket?.emitTableUpdate(ctx, table)
|
|
}
|
|
|
|
emitTableDeletion(ctx: any, table: Table) {
|
|
this.emitToRoom(ctx, ctx.appId, BuilderSocketEvent.TableChange, {
|
|
id: table._id,
|
|
table: null,
|
|
})
|
|
gridSocket?.emitTableDeletion(ctx, table)
|
|
}
|
|
|
|
emitDatasourceUpdate(ctx: any, datasource: Datasource) {
|
|
this.emitToRoom(ctx, ctx.appId, BuilderSocketEvent.DatasourceChange, {
|
|
id: datasource._id,
|
|
datasource,
|
|
})
|
|
}
|
|
|
|
emitDatasourceDeletion(ctx: any, id: string) {
|
|
this.emitToRoom(ctx, ctx.appId, BuilderSocketEvent.DatasourceChange, {
|
|
id,
|
|
datasource: null,
|
|
})
|
|
}
|
|
|
|
emitScreenUpdate(ctx: any, screen: Screen) {
|
|
this.emitToRoom(ctx, ctx.appId, BuilderSocketEvent.ScreenChange, {
|
|
id: screen._id,
|
|
screen,
|
|
})
|
|
}
|
|
|
|
emitScreenDeletion(ctx: any, id: string) {
|
|
this.emitToRoom(ctx, ctx.appId, BuilderSocketEvent.ScreenChange, {
|
|
id,
|
|
screen: null,
|
|
})
|
|
}
|
|
|
|
emitAppMetadataUpdate(ctx: any, metadata: Partial<App>) {
|
|
this.emitToRoom(ctx, ctx.appId, BuilderSocketEvent.AppMetadataChange, {
|
|
metadata,
|
|
})
|
|
}
|
|
|
|
emitAppPublish(ctx: any) {
|
|
this.emitToRoom(ctx, ctx.appId, BuilderSocketEvent.AppPublishChange, {
|
|
published: true,
|
|
user: ctx.user,
|
|
})
|
|
}
|
|
|
|
emitAppUnpublish(ctx: any) {
|
|
this.emitToRoom(ctx, ctx.appId, BuilderSocketEvent.AppPublishChange, {
|
|
published: false,
|
|
user: ctx.user,
|
|
})
|
|
}
|
|
|
|
emitAutomationUpdate(ctx: any, automation: Automation) {
|
|
this.emitToRoom(ctx, ctx.appId, BuilderSocketEvent.AutomationChange, {
|
|
id: automation._id,
|
|
automation,
|
|
})
|
|
}
|
|
|
|
emitAutomationDeletion(ctx: any, id: string) {
|
|
this.emitToRoom(ctx, ctx.appId, BuilderSocketEvent.AutomationChange, {
|
|
id,
|
|
automation: null,
|
|
})
|
|
}
|
|
}
|