2021-08-03 19:02:59 +00:00
|
|
|
//
|
2022-04-14 05:30:30 +00:00
|
|
|
// Copyright © 2022 Hardcore Engineering Inc.
|
2021-08-03 19:02:59 +00:00
|
|
|
//
|
|
|
|
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
|
|
|
// you may not use this file except in compliance with the License. You may
|
|
|
|
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
|
|
|
//
|
|
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
//
|
|
|
|
// See the License for the specific language governing permissions and
|
|
|
|
// limitations under the License.
|
|
|
|
//
|
|
|
|
|
2022-04-23 03:45:55 +00:00
|
|
|
import core, {
|
|
|
|
Class,
|
|
|
|
Doc,
|
|
|
|
DocumentQuery,
|
|
|
|
FindOptions,
|
|
|
|
FindResult,
|
|
|
|
MeasureContext,
|
|
|
|
ModelDb,
|
|
|
|
Ref,
|
|
|
|
Space,
|
2022-04-29 05:27:17 +00:00
|
|
|
Tx,
|
|
|
|
TxFactory,
|
2022-04-23 03:45:55 +00:00
|
|
|
TxResult
|
|
|
|
} from '@anticrm/core'
|
2021-12-22 09:02:51 +00:00
|
|
|
import { readRequest, Response, serialize, unknownError } from '@anticrm/platform'
|
2022-04-14 05:30:30 +00:00
|
|
|
import type { Pipeline, SessionContext } from '@anticrm/server-core'
|
2022-01-27 08:53:09 +00:00
|
|
|
import { decodeToken, Token } from '@anticrm/server-token'
|
2021-08-03 19:02:59 +00:00
|
|
|
import { createServer, IncomingMessage } from 'http'
|
2021-12-22 09:02:51 +00:00
|
|
|
import WebSocket, { Server } from 'ws'
|
2021-08-03 19:02:59 +00:00
|
|
|
|
2022-01-27 08:53:09 +00:00
|
|
|
let LOGGING_ENABLED = true
|
2021-08-11 09:35:56 +00:00
|
|
|
|
2022-04-23 03:45:55 +00:00
|
|
|
export function disableLogging (): void {
|
|
|
|
LOGGING_ENABLED = false
|
|
|
|
}
|
2021-08-11 09:35:56 +00:00
|
|
|
|
2021-12-22 09:02:51 +00:00
|
|
|
class Session {
|
2022-04-23 03:45:55 +00:00
|
|
|
readonly modelDb: ModelDb
|
|
|
|
|
2021-08-11 09:35:56 +00:00
|
|
|
constructor (
|
|
|
|
private readonly manager: SessionManager,
|
2021-08-28 07:08:41 +00:00
|
|
|
private readonly token: Token,
|
2022-04-14 05:30:30 +00:00
|
|
|
private readonly pipeline: Pipeline
|
2022-04-23 03:45:55 +00:00
|
|
|
) {
|
|
|
|
this.modelDb = pipeline.modelDb
|
|
|
|
}
|
2021-08-11 09:35:56 +00:00
|
|
|
|
2022-04-14 05:30:30 +00:00
|
|
|
getUser (): string {
|
|
|
|
return this.token.email
|
|
|
|
}
|
|
|
|
|
2022-04-23 03:45:55 +00:00
|
|
|
async ping (): Promise<string> {
|
|
|
|
console.log('ping')
|
|
|
|
return 'pong!'
|
|
|
|
}
|
2021-08-11 10:07:49 +00:00
|
|
|
|
2022-04-23 03:45:55 +00:00
|
|
|
async findAll<T extends Doc>(
|
|
|
|
ctx: MeasureContext,
|
|
|
|
_class: Ref<Class<T>>,
|
|
|
|
query: DocumentQuery<T>,
|
|
|
|
options?: FindOptions<T>
|
|
|
|
): Promise<FindResult<T>> {
|
2022-04-14 05:30:30 +00:00
|
|
|
const context = ctx as SessionContext
|
|
|
|
context.userEmail = this.token.email
|
|
|
|
return await this.pipeline.findAll(context, _class, query, options)
|
2021-08-11 09:35:56 +00:00
|
|
|
}
|
|
|
|
|
2021-12-22 09:02:51 +00:00
|
|
|
async tx (ctx: MeasureContext, tx: Tx): Promise<TxResult> {
|
2022-04-14 05:30:30 +00:00
|
|
|
const context = ctx as SessionContext
|
|
|
|
context.userEmail = this.token.email
|
|
|
|
const [result, derived, target] = await this.pipeline.tx(context, tx)
|
2021-12-22 09:02:51 +00:00
|
|
|
|
2022-04-23 03:45:55 +00:00
|
|
|
this.manager.broadcast(this, this.token.workspace, { result: tx }, target)
|
2021-12-22 09:02:51 +00:00
|
|
|
for (const dtx of derived) {
|
2022-04-23 03:45:55 +00:00
|
|
|
this.manager.broadcast(null, this.token.workspace, { result: dtx }, target)
|
2021-08-11 09:35:56 +00:00
|
|
|
}
|
2021-10-13 18:58:14 +00:00
|
|
|
return result
|
2021-08-11 09:35:56 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
interface Workspace {
|
2022-04-14 05:30:30 +00:00
|
|
|
pipeline: Pipeline
|
2021-08-11 09:35:56 +00:00
|
|
|
sessions: [Session, WebSocket][]
|
|
|
|
}
|
|
|
|
|
|
|
|
class SessionManager {
|
|
|
|
private readonly workspaces = new Map<string, Workspace>()
|
|
|
|
|
2022-04-23 03:45:55 +00:00
|
|
|
async addSession (
|
|
|
|
ctx: MeasureContext,
|
|
|
|
ws: WebSocket,
|
|
|
|
token: Token,
|
|
|
|
pipelineFactory: (ws: string) => Promise<Pipeline>
|
|
|
|
): Promise<Session> {
|
2021-08-11 09:35:56 +00:00
|
|
|
const workspace = this.workspaces.get(token.workspace)
|
|
|
|
if (workspace === undefined) {
|
2022-04-23 03:45:55 +00:00
|
|
|
return await this.createWorkspace(ctx, pipelineFactory, token, ws)
|
2021-08-11 09:35:56 +00:00
|
|
|
} else {
|
2022-01-27 08:53:09 +00:00
|
|
|
if (token.extra?.model === 'reload') {
|
|
|
|
console.log('reloading workspace', JSON.stringify(token))
|
|
|
|
// If upgrade client is used.
|
|
|
|
// Drop all existing clients
|
|
|
|
if (workspace.sessions.length > 0) {
|
|
|
|
for (const s of workspace.sessions) {
|
2022-04-23 03:45:55 +00:00
|
|
|
await this.close(ctx, s[1], token.workspace, 0, 'upgrade')
|
2022-01-27 08:53:09 +00:00
|
|
|
}
|
|
|
|
}
|
2022-04-23 03:45:55 +00:00
|
|
|
return await this.createWorkspace(ctx, pipelineFactory, token, ws)
|
2022-01-27 08:53:09 +00:00
|
|
|
}
|
|
|
|
|
2022-04-14 05:30:30 +00:00
|
|
|
const session = new Session(this, token, workspace.pipeline)
|
2021-08-11 09:35:56 +00:00
|
|
|
workspace.sessions.push([session, ws])
|
2022-04-23 03:45:55 +00:00
|
|
|
await this.setStatus(ctx, session, true)
|
2021-08-11 09:35:56 +00:00
|
|
|
return session
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-23 03:45:55 +00:00
|
|
|
private async setStatus (ctx: MeasureContext, session: Session, online: boolean): Promise<void> {
|
|
|
|
try {
|
|
|
|
const user = (
|
|
|
|
await session.modelDb.findAll(
|
|
|
|
core.class.Account,
|
|
|
|
{
|
|
|
|
email: session.getUser()
|
|
|
|
},
|
|
|
|
{ limit: 1 }
|
|
|
|
)
|
|
|
|
)[0]
|
|
|
|
if (user === undefined) return
|
|
|
|
const status = (await session.findAll(ctx, core.class.UserStatus, { modifiedBy: user._id }, { limit: 1 }))[0]
|
|
|
|
const txFactory = new TxFactory(user._id)
|
|
|
|
if (status === undefined) {
|
|
|
|
const tx = txFactory.createTxCreateDoc(core.class.UserStatus, user._id as string as Ref<Space>, {
|
|
|
|
online
|
|
|
|
})
|
|
|
|
tx.space = core.space.DerivedTx
|
|
|
|
await session.tx(ctx, tx)
|
|
|
|
} else if (status.online !== online) {
|
|
|
|
const tx = txFactory.createTxUpdateDoc(status._class, status.space, status._id, {
|
|
|
|
online
|
|
|
|
})
|
|
|
|
tx.space = core.space.DerivedTx
|
|
|
|
await session.tx(ctx, tx)
|
|
|
|
}
|
2022-04-29 05:27:17 +00:00
|
|
|
} catch {}
|
2022-04-23 03:45:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private async createWorkspace (
|
|
|
|
ctx: MeasureContext,
|
|
|
|
pipelineFactory: (ws: string) => Promise<Pipeline>,
|
|
|
|
token: Token,
|
|
|
|
ws: WebSocket
|
|
|
|
): Promise<Session> {
|
2022-04-14 05:30:30 +00:00
|
|
|
const pipeline = await pipelineFactory(token.workspace)
|
|
|
|
const session = new Session(this, token, pipeline)
|
2022-01-27 08:53:09 +00:00
|
|
|
const workspace: Workspace = {
|
2022-04-14 05:30:30 +00:00
|
|
|
pipeline,
|
2022-01-27 08:53:09 +00:00
|
|
|
sessions: [[session, ws]]
|
|
|
|
}
|
|
|
|
this.workspaces.set(token.workspace, workspace)
|
2022-04-23 03:45:55 +00:00
|
|
|
await this.setStatus(ctx, session, true)
|
2022-01-27 08:53:09 +00:00
|
|
|
return session
|
|
|
|
}
|
|
|
|
|
2022-04-23 03:45:55 +00:00
|
|
|
async close (ctx: MeasureContext, ws: WebSocket, workspaceId: string, code: number, reason: string): Promise<void> {
|
2021-08-11 09:35:56 +00:00
|
|
|
if (LOGGING_ENABLED) console.log(`closing websocket, code: ${code}, reason: ${reason}`)
|
2022-01-27 08:53:09 +00:00
|
|
|
const workspace = this.workspaces.get(workspaceId)
|
2021-08-11 09:35:56 +00:00
|
|
|
if (workspace === undefined) {
|
2022-01-27 08:53:09 +00:00
|
|
|
console.error(new Error('internal: cannot find sessions'))
|
|
|
|
return
|
2021-08-11 09:35:56 +00:00
|
|
|
}
|
2022-04-23 03:45:55 +00:00
|
|
|
const index = workspace.sessions.findIndex((p) => p[1] === ws)
|
|
|
|
if (index !== -1) {
|
|
|
|
const session = workspace.sessions[index]
|
|
|
|
workspace.sessions.splice(index, 1)
|
|
|
|
const user = session[0].getUser()
|
|
|
|
const another = workspace.sessions.findIndex((p) => p[0].getUser() === user)
|
|
|
|
if (another === -1) {
|
|
|
|
await this.setStatus(ctx, session[0], false)
|
|
|
|
}
|
|
|
|
if (workspace.sessions.length === 0) {
|
|
|
|
if (LOGGING_ENABLED) console.log('no sessions for workspace', workspaceId)
|
|
|
|
this.workspaces.delete(workspaceId)
|
|
|
|
workspace.pipeline.close().catch((err) => console.error(err))
|
|
|
|
}
|
2021-08-11 09:35:56 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-23 03:45:55 +00:00
|
|
|
broadcast (from: Session | null, workspaceId: string, resp: Response<any>, target?: string): void {
|
|
|
|
const workspace = this.workspaces.get(workspaceId)
|
2021-08-11 09:35:56 +00:00
|
|
|
if (workspace === undefined) {
|
2022-01-27 08:53:09 +00:00
|
|
|
console.error(new Error('internal: cannot find sessions'))
|
|
|
|
return
|
2021-08-11 09:35:56 +00:00
|
|
|
}
|
|
|
|
if (LOGGING_ENABLED) console.log(`server broadcasting to ${workspace.sessions.length} clients...`)
|
|
|
|
const msg = serialize(resp)
|
|
|
|
for (const session of workspace.sessions) {
|
2022-04-14 05:30:30 +00:00
|
|
|
if (session[0] !== from) {
|
|
|
|
if (target === undefined) {
|
|
|
|
session[1].send(msg)
|
|
|
|
} else if (session[0].getUser() === target) {
|
|
|
|
session[1].send(msg)
|
|
|
|
}
|
|
|
|
}
|
2021-08-11 09:35:56 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-23 03:45:55 +00:00
|
|
|
async function handleRequest<S extends Session> (
|
|
|
|
ctx: MeasureContext,
|
|
|
|
service: S,
|
|
|
|
ws: WebSocket,
|
|
|
|
msg: string
|
|
|
|
): Promise<void> {
|
2021-08-03 19:02:59 +00:00
|
|
|
const request = readRequest(msg)
|
|
|
|
const f = (service as any)[request.method]
|
2021-12-06 15:16:38 +00:00
|
|
|
try {
|
2021-12-22 09:02:51 +00:00
|
|
|
const params = [ctx, ...request.params]
|
|
|
|
const result = await f.apply(service, params)
|
|
|
|
const resp: Response<any> = { id: request.id, result }
|
2021-12-06 15:16:38 +00:00
|
|
|
ws.send(serialize(resp))
|
|
|
|
} catch (err: any) {
|
2021-12-22 09:02:51 +00:00
|
|
|
const resp: Response<any> = {
|
|
|
|
id: request.id,
|
|
|
|
error: unknownError(err)
|
|
|
|
}
|
2021-12-06 15:16:38 +00:00
|
|
|
ws.send(serialize(resp))
|
|
|
|
}
|
2021-08-03 19:02:59 +00:00
|
|
|
}
|
|
|
|
|
2021-08-04 16:17:01 +00:00
|
|
|
/**
|
|
|
|
* @public
|
|
|
|
* @param sessionFactory -
|
2021-08-04 09:10:22 +00:00
|
|
|
* @param port -
|
|
|
|
* @param host -
|
2021-08-03 19:02:59 +00:00
|
|
|
*/
|
2022-04-23 03:45:55 +00:00
|
|
|
export function start (
|
|
|
|
ctx: MeasureContext,
|
|
|
|
pipelineFactory: (workspace: string) => Promise<Pipeline>,
|
|
|
|
port: number,
|
|
|
|
host?: string
|
|
|
|
): () => void {
|
2021-08-04 20:47:15 +00:00
|
|
|
console.log(`starting server on port ${port} ...`)
|
2021-08-03 19:02:59 +00:00
|
|
|
|
2021-08-11 09:35:56 +00:00
|
|
|
const sessions = new SessionManager()
|
2021-08-03 19:02:59 +00:00
|
|
|
|
|
|
|
const wss = new Server({ noServer: true })
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
2021-08-28 07:08:41 +00:00
|
|
|
wss.on('connection', async (ws: WebSocket, request: any, token: Token) => {
|
2021-08-19 17:34:58 +00:00
|
|
|
const buffer: string[] = []
|
|
|
|
|
2022-04-23 03:45:55 +00:00
|
|
|
ws.on('message', (msg: string) => {
|
|
|
|
buffer.push(msg)
|
|
|
|
})
|
|
|
|
const session = await sessions.addSession(ctx, ws, token, pipelineFactory)
|
2021-08-03 19:02:59 +00:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
2021-12-22 09:02:51 +00:00
|
|
|
ws.on('message', async (msg: string) => await handleRequest(ctx, session, ws, msg))
|
2022-04-23 03:45:55 +00:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
|
|
|
ws.on('close', async (code: number, reason: string) => await sessions.close(ctx, ws, token.workspace, code, reason))
|
2021-08-19 17:34:58 +00:00
|
|
|
|
|
|
|
for (const msg of buffer) {
|
2021-12-22 09:02:51 +00:00
|
|
|
await handleRequest(ctx, session, ws, msg)
|
2021-08-19 17:34:58 +00:00
|
|
|
}
|
2021-08-03 19:02:59 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
const server = createServer()
|
2021-08-07 05:58:28 +00:00
|
|
|
server.on('upgrade', (request: IncomingMessage, socket: any, head: Buffer) => {
|
2021-08-03 19:02:59 +00:00
|
|
|
const token = request.url?.substring(1) // remove leading '/'
|
2021-08-04 07:56:34 +00:00
|
|
|
try {
|
2022-01-27 08:53:09 +00:00
|
|
|
const payload = decodeToken(token ?? '')
|
2021-08-08 21:04:39 +00:00
|
|
|
console.log('client connected with payload', payload)
|
2022-04-23 03:45:55 +00:00
|
|
|
wss.handleUpgrade(request, socket, head, (ws) => wss.emit('connection', ws, request, payload))
|
2021-08-04 07:56:34 +00:00
|
|
|
} catch (err) {
|
2021-08-11 09:35:56 +00:00
|
|
|
console.log('unauthorized client')
|
2021-08-03 19:02:59 +00:00
|
|
|
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n')
|
|
|
|
socket.destroy()
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
server.listen(port, host)
|
2021-11-22 11:17:10 +00:00
|
|
|
return () => {
|
|
|
|
server.close()
|
|
|
|
}
|
2021-08-03 19:02:59 +00:00
|
|
|
}
|