2021-08-03 22:02:59 +03:00
|
|
|
//
|
2022-04-14 08:30:30 +03:00
|
|
|
// Copyright © 2022 Hardcore Engineering Inc.
|
2021-08-03 22:02:59 +03: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.
|
|
|
|
//
|
|
|
|
|
2023-01-04 20:58:54 +03:00
|
|
|
import core, {
|
2023-04-20 13:11:22 +03:00
|
|
|
metricsAggregate,
|
2023-01-04 20:58:54 +03:00
|
|
|
generateId,
|
|
|
|
MeasureContext,
|
|
|
|
Ref,
|
|
|
|
Space,
|
|
|
|
toWorkspaceString,
|
2023-02-15 06:14:20 +03:00
|
|
|
Tx,
|
2023-01-04 20:58:54 +03:00
|
|
|
TxFactory,
|
|
|
|
WorkspaceId
|
|
|
|
} from '@hcengineering/core'
|
2022-09-21 11:08:25 +03:00
|
|
|
import { readRequest, Response, serialize, UNAUTHORIZED, unknownError } from '@hcengineering/platform'
|
2023-03-24 07:53:12 +03:00
|
|
|
import type { Pipeline, SessionContext } from '@hcengineering/server-core'
|
2022-09-21 11:08:25 +03:00
|
|
|
import { decodeToken, Token } from '@hcengineering/server-token'
|
2023-04-20 13:11:22 +03:00
|
|
|
import { createServer, IncomingMessage, ServerResponse } from 'http'
|
2023-03-24 07:53:12 +03:00
|
|
|
import WebSocket, { RawData, WebSocketServer } from 'ws'
|
2023-02-15 06:14:20 +03:00
|
|
|
import { BroadcastCall, PipelineFactory, Session } from './types'
|
2021-08-03 22:02:59 +03:00
|
|
|
|
2022-01-27 11:53:09 +03:00
|
|
|
let LOGGING_ENABLED = true
|
2021-08-11 12:35:56 +03:00
|
|
|
|
2022-04-23 06:45:55 +03:00
|
|
|
export function disableLogging (): void {
|
|
|
|
LOGGING_ENABLED = false
|
|
|
|
}
|
2021-08-11 12:35:56 +03:00
|
|
|
|
2023-03-15 19:36:50 +03:00
|
|
|
function timeoutPromise (time: number): Promise<void> {
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
setTimeout(resolve, time)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-08-11 12:35:56 +03:00
|
|
|
interface Workspace {
|
2023-01-04 20:58:54 +03:00
|
|
|
id: string
|
|
|
|
pipeline: Promise<Pipeline>
|
2021-08-11 12:35:56 +03:00
|
|
|
sessions: [Session, WebSocket][]
|
2022-05-23 18:53:33 +03:00
|
|
|
upgrade: boolean
|
2023-01-04 20:58:54 +03:00
|
|
|
closing?: Promise<void>
|
2021-08-11 12:35:56 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
class SessionManager {
|
2023-04-20 13:11:22 +03:00
|
|
|
readonly workspaces = new Map<string, Workspace>()
|
2021-08-11 12:35:56 +03:00
|
|
|
|
2022-05-23 18:53:33 +03:00
|
|
|
constructor (readonly sessionFactory: (token: Token, pipeline: Pipeline, broadcast: BroadcastCall) => Session) {}
|
|
|
|
|
|
|
|
createSession (token: Token, pipeline: Pipeline): Session {
|
|
|
|
return this.sessionFactory(token, pipeline, this.broadcast.bind(this))
|
|
|
|
}
|
|
|
|
|
2023-03-24 07:53:12 +03:00
|
|
|
upgradeId: string | undefined
|
|
|
|
|
2022-04-23 06:45:55 +03:00
|
|
|
async addSession (
|
|
|
|
ctx: MeasureContext,
|
|
|
|
ws: WebSocket,
|
|
|
|
token: Token,
|
2023-02-15 06:14:20 +03:00
|
|
|
pipelineFactory: PipelineFactory,
|
2023-02-28 10:02:15 +03:00
|
|
|
productId: string,
|
|
|
|
sessionId?: string
|
2022-04-23 06:45:55 +03:00
|
|
|
): Promise<Session> {
|
2022-11-16 18:03:03 +03:00
|
|
|
const wsString = toWorkspaceString(token.workspace, '@')
|
2023-01-04 20:58:54 +03:00
|
|
|
|
|
|
|
let workspace = this.workspaces.get(wsString)
|
|
|
|
await workspace?.closing
|
|
|
|
workspace = this.workspaces.get(wsString)
|
|
|
|
|
2021-08-11 12:35:56 +03:00
|
|
|
if (workspace === undefined) {
|
2023-03-02 06:31:47 +03:00
|
|
|
workspace = this.createWorkspace(ctx, pipelineFactory, token)
|
2023-01-04 20:58:54 +03:00
|
|
|
}
|
2022-01-27 11:53:09 +03:00
|
|
|
|
2023-01-04 20:58:54 +03:00
|
|
|
if (token.extra?.model === 'upgrade') {
|
2023-04-20 13:11:22 +03:00
|
|
|
if (LOGGING_ENABLED) console.log(token.workspace.name, 'reloading workspace', JSON.stringify(token))
|
2023-03-24 07:53:12 +03:00
|
|
|
this.upgradeId = sessionId
|
2023-01-04 20:58:54 +03:00
|
|
|
// If upgrade client is used.
|
|
|
|
// Drop all existing clients
|
|
|
|
await this.closeAll(ctx, wsString, workspace, 0, 'upgrade')
|
|
|
|
// Wipe workspace and update values.
|
|
|
|
if (!workspace.upgrade) {
|
|
|
|
// This is previous workspace, intended to be closed.
|
|
|
|
workspace.id = generateId()
|
|
|
|
workspace.sessions = []
|
|
|
|
workspace.upgrade = token.extra?.model === 'upgrade'
|
2022-05-23 18:53:33 +03:00
|
|
|
}
|
2023-04-20 13:11:22 +03:00
|
|
|
if (LOGGING_ENABLED) console.log(token.workspace.name, 'no sessions for workspace', wsString)
|
2023-01-04 20:58:54 +03:00
|
|
|
// Re-create pipeline.
|
2023-03-02 06:31:47 +03:00
|
|
|
workspace.pipeline = pipelineFactory(ctx, token.workspace, true, (tx) =>
|
|
|
|
this.broadcastAll(workspace as Workspace, tx)
|
|
|
|
)
|
2022-05-23 18:53:33 +03:00
|
|
|
|
2023-01-04 20:58:54 +03:00
|
|
|
const pipeline = await workspace.pipeline
|
|
|
|
const session = this.createSession(token, pipeline)
|
2021-08-11 12:35:56 +03:00
|
|
|
workspace.sessions.push([session, ws])
|
|
|
|
return session
|
|
|
|
}
|
2023-01-04 20:58:54 +03:00
|
|
|
|
2023-03-24 07:53:12 +03:00
|
|
|
if (workspace.upgrade && sessionId !== this.upgradeId) {
|
2023-01-04 20:58:54 +03:00
|
|
|
ws.close()
|
|
|
|
throw new Error('Upgrade in progress....')
|
|
|
|
}
|
|
|
|
|
|
|
|
const pipeline = await workspace.pipeline
|
2023-02-28 10:02:15 +03:00
|
|
|
|
|
|
|
if (sessionId !== undefined) {
|
|
|
|
// try restore session
|
|
|
|
const existingSession = workspace.sessions.find((it) => it[0].sessionId === sessionId)
|
|
|
|
if (existingSession !== undefined) {
|
2023-03-24 07:53:12 +03:00
|
|
|
if (LOGGING_ENABLED) {
|
|
|
|
console.log(
|
2023-04-20 13:11:22 +03:00
|
|
|
token.workspace.name,
|
2023-03-24 07:53:12 +03:00
|
|
|
'found existing session',
|
|
|
|
token.email,
|
|
|
|
existingSession[0].sessionId,
|
|
|
|
existingSession[0].sessionInstanceId
|
|
|
|
)
|
|
|
|
}
|
2023-02-28 10:02:15 +03:00
|
|
|
// Update websocket
|
|
|
|
clearTimeout(existingSession[0].closeTimeout)
|
|
|
|
existingSession[0].closeTimeout = undefined
|
|
|
|
existingSession[1] = ws
|
|
|
|
return existingSession[0]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-04 20:58:54 +03:00
|
|
|
const session = this.createSession(token, pipeline)
|
2023-02-28 10:02:15 +03:00
|
|
|
session.sessionId = sessionId
|
2023-03-24 07:53:12 +03:00
|
|
|
session.sessionInstanceId = generateId()
|
2023-01-04 20:58:54 +03:00
|
|
|
workspace.sessions.push([session, ws])
|
|
|
|
await this.setStatus(ctx, session, true)
|
|
|
|
return session
|
|
|
|
}
|
|
|
|
|
2023-02-15 06:14:20 +03:00
|
|
|
broadcastAll (workspace: Workspace, tx: Tx[]): void {
|
|
|
|
for (const _tx of tx) {
|
|
|
|
const msg = serialize({ result: _tx })
|
|
|
|
for (const session of workspace.sessions) {
|
|
|
|
session[1].send(msg)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-02 06:31:47 +03:00
|
|
|
private createWorkspace (ctx: MeasureContext, pipelineFactory: PipelineFactory, token: Token): Workspace {
|
2023-01-04 20:58:54 +03:00
|
|
|
const upgrade = token.extra?.model === 'upgrade'
|
2023-02-15 06:14:20 +03:00
|
|
|
const workspace: Workspace = {
|
2023-01-04 20:58:54 +03:00
|
|
|
id: generateId(),
|
2023-03-02 06:31:47 +03:00
|
|
|
pipeline: pipelineFactory(ctx, token.workspace, upgrade, (tx) => this.broadcastAll(workspace, tx)),
|
2023-01-04 20:58:54 +03:00
|
|
|
sessions: [],
|
|
|
|
upgrade
|
|
|
|
}
|
2023-04-13 20:39:20 +03:00
|
|
|
if (LOGGING_ENABLED) console.time(token.workspace.name)
|
|
|
|
if (LOGGING_ENABLED) console.timeLog(token.workspace.name, 'Creating Workspace:', workspace.id)
|
2023-01-04 20:58:54 +03:00
|
|
|
this.workspaces.set(toWorkspaceString(token.workspace), workspace)
|
|
|
|
return workspace
|
2021-08-11 12:35:56 +03:00
|
|
|
}
|
|
|
|
|
2022-04-23 06:45:55 +03:00
|
|
|
private async setStatus (ctx: MeasureContext, session: Session, online: boolean): Promise<void> {
|
|
|
|
try {
|
|
|
|
const user = (
|
2023-03-01 19:46:36 +03:00
|
|
|
await session.findAll(
|
|
|
|
ctx,
|
2022-04-23 06:45:55 +03:00
|
|
|
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]
|
2023-04-05 10:12:06 +03:00
|
|
|
const txFactory = new TxFactory(user._id, true)
|
2022-04-23 06:45:55 +03:00
|
|
|
if (status === undefined) {
|
|
|
|
const tx = txFactory.createTxCreateDoc(core.class.UserStatus, user._id as string as Ref<Space>, {
|
|
|
|
online
|
|
|
|
})
|
|
|
|
await session.tx(ctx, tx)
|
|
|
|
} else if (status.online !== online) {
|
|
|
|
const tx = txFactory.createTxUpdateDoc(status._class, status.space, status._id, {
|
|
|
|
online
|
|
|
|
})
|
|
|
|
await session.tx(ctx, tx)
|
|
|
|
}
|
2022-04-29 08:27:17 +03:00
|
|
|
} catch {}
|
2022-04-23 06:45:55 +03:00
|
|
|
}
|
|
|
|
|
2022-11-16 18:03:03 +03:00
|
|
|
async close (
|
|
|
|
ctx: MeasureContext,
|
|
|
|
ws: WebSocket,
|
|
|
|
workspaceId: WorkspaceId,
|
|
|
|
code: number,
|
|
|
|
reason: string
|
|
|
|
): Promise<void> {
|
2023-04-20 13:11:22 +03:00
|
|
|
if (LOGGING_ENABLED) console.log(workspaceId.name, `closing websocket, code: ${code}, reason: ${reason}`)
|
2022-11-16 18:03:03 +03:00
|
|
|
const wsid = toWorkspaceString(workspaceId)
|
|
|
|
const workspace = this.workspaces.get(wsid)
|
2021-08-11 12:35:56 +03:00
|
|
|
if (workspace === undefined) {
|
2023-04-13 20:39:20 +03:00
|
|
|
if (LOGGING_ENABLED) console.error(new Error('internal: cannot find sessions'))
|
2022-01-27 11:53:09 +03:00
|
|
|
return
|
2021-08-11 12:35:56 +03:00
|
|
|
}
|
2022-04-23 06:45:55 +03:00
|
|
|
const index = workspace.sessions.findIndex((p) => p[1] === ws)
|
|
|
|
if (index !== -1) {
|
|
|
|
const session = workspace.sessions[index]
|
|
|
|
workspace.sessions.splice(index, 1)
|
2022-06-01 15:05:07 +03:00
|
|
|
session[1].close()
|
2022-04-23 06:45:55 +03:00
|
|
|
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) {
|
2023-04-13 20:39:20 +03:00
|
|
|
const wsUID = workspace.id
|
2023-04-20 13:11:22 +03:00
|
|
|
if (LOGGING_ENABLED) console.log(workspaceId.name, 'no sessions for workspace', wsid, wsUID)
|
2023-01-04 20:58:54 +03:00
|
|
|
|
2023-03-15 19:36:50 +03:00
|
|
|
const waitAndClose = async (workspace: Workspace): Promise<void> => {
|
|
|
|
try {
|
|
|
|
const pl = await workspace.pipeline
|
|
|
|
await Promise.race([pl, timeoutPromise(60000)])
|
|
|
|
await Promise.race([pl.close(), timeoutPromise(60000)])
|
|
|
|
|
2023-04-13 20:39:20 +03:00
|
|
|
if (this.workspaces.get(wsid)?.id === wsUID) {
|
2023-03-15 19:36:50 +03:00
|
|
|
this.workspaces.delete(wsid)
|
|
|
|
}
|
2023-04-13 20:39:20 +03:00
|
|
|
if (LOGGING_ENABLED) console.timeLog(workspaceId.name, 'Closed workspace', wsUID)
|
2023-03-15 19:36:50 +03:00
|
|
|
} catch (err: any) {
|
2023-01-04 20:58:54 +03:00
|
|
|
this.workspaces.delete(wsid)
|
2023-04-20 13:11:22 +03:00
|
|
|
if (LOGGING_ENABLED) console.error(workspaceId.name, err)
|
2023-01-04 20:58:54 +03:00
|
|
|
}
|
2023-03-15 19:36:50 +03:00
|
|
|
}
|
|
|
|
workspace.closing = waitAndClose(workspace)
|
2023-01-04 20:58:54 +03:00
|
|
|
await workspace.closing
|
2022-04-23 06:45:55 +03:00
|
|
|
}
|
2021-08-11 12:35:56 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-24 07:53:12 +03:00
|
|
|
async closeAll (
|
|
|
|
ctx: MeasureContext,
|
|
|
|
wsId: string,
|
|
|
|
workspace: Workspace,
|
|
|
|
code: number,
|
|
|
|
reason: 'upgrade' | 'shutdown'
|
|
|
|
): Promise<void> {
|
2023-04-13 20:39:20 +03:00
|
|
|
if (LOGGING_ENABLED) console.timeLog(wsId, `closing workspace ${workspace.id}, code: ${code}, reason: ${reason}`)
|
2023-01-04 20:58:54 +03:00
|
|
|
|
|
|
|
const sessions = Array.from(workspace.sessions)
|
|
|
|
workspace.sessions = []
|
|
|
|
|
2023-01-26 16:53:00 +03:00
|
|
|
const closeS = async (s: Session, webSocket: WebSocket): Promise<void> => {
|
2023-02-28 10:02:15 +03:00
|
|
|
clearTimeout(s.closeTimeout)
|
2023-03-24 07:53:12 +03:00
|
|
|
s.workspaceClosed = true
|
|
|
|
if (reason === 'upgrade') {
|
|
|
|
// await for message to go to client.
|
|
|
|
await new Promise((resolve) => {
|
|
|
|
// Override message handler, to wait for upgrading response from clients.
|
|
|
|
webSocket.on('close', () => {
|
|
|
|
resolve(null)
|
2023-01-04 20:58:54 +03:00
|
|
|
})
|
2023-03-24 07:53:12 +03:00
|
|
|
webSocket.send(
|
|
|
|
serialize({
|
|
|
|
result: {
|
|
|
|
_class: core.class.TxModelUpgrade
|
|
|
|
}
|
|
|
|
})
|
|
|
|
)
|
|
|
|
setTimeout(resolve, 1000)
|
|
|
|
})
|
|
|
|
}
|
2023-01-26 16:53:00 +03:00
|
|
|
webSocket.close()
|
|
|
|
await this.setStatus(ctx, s, false)
|
2023-01-04 20:58:54 +03:00
|
|
|
}
|
2023-01-26 16:53:00 +03:00
|
|
|
|
2023-04-13 20:39:20 +03:00
|
|
|
if (LOGGING_ENABLED) console.timeLog(wsId, workspace.id, 'Clients disconnected. Closing Workspace...')
|
2023-01-26 16:53:00 +03:00
|
|
|
await Promise.all(sessions.map((s) => closeS(s[0], s[1])))
|
|
|
|
|
|
|
|
const closePipeline = async (): Promise<void> => {
|
|
|
|
try {
|
2023-04-13 20:39:20 +03:00
|
|
|
if (LOGGING_ENABLED) console.timeLog(wsId, 'closing pipeline')
|
2023-01-26 16:53:00 +03:00
|
|
|
await (await workspace.pipeline).close()
|
2023-04-13 20:39:20 +03:00
|
|
|
if (LOGGING_ENABLED) console.timeLog(wsId, 'closing pipeline done')
|
2023-01-26 16:53:00 +03:00
|
|
|
} catch (err: any) {
|
|
|
|
console.error(err)
|
|
|
|
}
|
2023-01-04 20:58:54 +03:00
|
|
|
}
|
2023-03-15 19:36:50 +03:00
|
|
|
await Promise.race([closePipeline(), timeoutPromise(15000)])
|
2023-04-13 20:39:20 +03:00
|
|
|
if (LOGGING_ENABLED) console.timeLog(wsId, 'Workspace closed...')
|
|
|
|
console.timeEnd(wsId)
|
2023-01-04 20:58:54 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
async closeWorkspaces (ctx: MeasureContext): Promise<void> {
|
|
|
|
for (const w of this.workspaces) {
|
|
|
|
await this.closeAll(ctx, w[0], w[1], 1, 'shutdown')
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-02 06:31:47 +03:00
|
|
|
broadcast (from: Session | null, workspaceId: WorkspaceId, resp: Response<any>, target?: string[]): void {
|
2022-11-16 18:03:03 +03:00
|
|
|
const workspace = this.workspaces.get(toWorkspaceString(workspaceId))
|
2021-08-11 12:35:56 +03:00
|
|
|
if (workspace === undefined) {
|
2022-01-27 11:53:09 +03:00
|
|
|
console.error(new Error('internal: cannot find sessions'))
|
|
|
|
return
|
2021-08-11 12:35:56 +03:00
|
|
|
}
|
2023-04-20 13:11:22 +03:00
|
|
|
if (LOGGING_ENABLED) console.log(workspaceId.name, `server broadcasting to ${workspace.sessions.length} clients...`)
|
2021-08-11 12:35:56 +03:00
|
|
|
const msg = serialize(resp)
|
|
|
|
for (const session of workspace.sessions) {
|
2022-04-14 08:30:30 +03:00
|
|
|
if (session[0] !== from) {
|
|
|
|
if (target === undefined) {
|
|
|
|
session[1].send(msg)
|
2023-03-02 06:31:47 +03:00
|
|
|
} else if (target.includes(session[0].getUser())) {
|
2022-04-14 08:30:30 +03:00
|
|
|
session[1].send(msg)
|
|
|
|
}
|
|
|
|
}
|
2021-08-11 12:35:56 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-23 06:45:55 +03:00
|
|
|
async function handleRequest<S extends Session> (
|
|
|
|
ctx: MeasureContext,
|
|
|
|
service: S,
|
|
|
|
ws: WebSocket,
|
2023-04-05 11:47:10 +03:00
|
|
|
msg: string,
|
|
|
|
workspace: string
|
2022-04-23 06:45:55 +03:00
|
|
|
): Promise<void> {
|
2021-08-03 22:02:59 +03:00
|
|
|
const request = readRequest(msg)
|
2022-06-01 15:05:07 +03:00
|
|
|
if (request.id === -1 && request.method === 'hello') {
|
2023-04-13 20:39:20 +03:00
|
|
|
if (LOGGING_ENABLED) console.timeLog(workspace, 'hello happen', service.getUser())
|
2022-06-01 15:05:07 +03:00
|
|
|
ws.send(serialize({ id: -1, result: 'hello' }))
|
|
|
|
return
|
|
|
|
}
|
2023-01-04 20:58:54 +03:00
|
|
|
if (request.id === -1 && request.method === '#upgrade') {
|
|
|
|
ws.close(0, 'upgrade')
|
|
|
|
return
|
|
|
|
}
|
2023-04-20 13:11:22 +03:00
|
|
|
const userCtx = ctx.newChild('client', { workspace }) as SessionContext
|
2023-03-24 07:53:12 +03:00
|
|
|
userCtx.sessionId = service.sessionInstanceId ?? ''
|
2021-08-03 22:02:59 +03:00
|
|
|
const f = (service as any)[request.method]
|
2023-04-11 20:43:37 +03:00
|
|
|
let timeout: any
|
|
|
|
let hangTimeout: any
|
2021-12-06 18:16:38 +03:00
|
|
|
try {
|
2023-03-24 07:53:12 +03:00
|
|
|
const params = [userCtx, ...request.params]
|
2023-04-05 11:47:10 +03:00
|
|
|
|
|
|
|
const st = Date.now()
|
2023-04-11 20:43:37 +03:00
|
|
|
timeout = setTimeout(() => {
|
2023-04-13 20:39:20 +03:00
|
|
|
if (LOGGING_ENABLED) console.timeLog(workspace, 'long request found', service.getUser(), request, params)
|
2023-04-05 11:47:10 +03:00
|
|
|
}, 4000)
|
|
|
|
|
2023-04-11 20:43:37 +03:00
|
|
|
hangTimeout = setTimeout(() => {
|
2023-04-13 20:39:20 +03:00
|
|
|
if (LOGGING_ENABLED) {
|
|
|
|
console.timeLog(workspace, 'request hang found, 30sec', workspace, service.getUser(), request, params)
|
|
|
|
}
|
2023-04-05 11:47:10 +03:00
|
|
|
}, 30000)
|
|
|
|
|
2023-04-14 20:18:23 +03:00
|
|
|
let result = await f.apply(service, params)
|
2023-04-05 11:47:10 +03:00
|
|
|
clearTimeout(timeout)
|
|
|
|
clearTimeout(hangTimeout)
|
2021-12-22 12:02:51 +03:00
|
|
|
const resp: Response<any> = { id: request.id, result }
|
2023-04-05 11:47:10 +03:00
|
|
|
|
|
|
|
const diff = Date.now() - st
|
2023-04-13 20:39:20 +03:00
|
|
|
if (diff > 5000 && LOGGING_ENABLED) {
|
|
|
|
console.timeLog(
|
|
|
|
timeout,
|
2023-04-05 11:47:10 +03:00
|
|
|
'very long request found',
|
|
|
|
workspace,
|
|
|
|
service.getUser(),
|
|
|
|
request,
|
|
|
|
params,
|
|
|
|
Array.isArray(result) ? result.length : '0',
|
|
|
|
diff
|
|
|
|
)
|
|
|
|
}
|
2023-04-14 20:18:23 +03:00
|
|
|
const toSend = serialize(resp)
|
|
|
|
// Clear for gc to make work
|
|
|
|
resp.result = undefined
|
|
|
|
result = undefined
|
|
|
|
ws.send(toSend)
|
2021-12-06 18:16:38 +03:00
|
|
|
} catch (err: any) {
|
2023-04-13 20:39:20 +03:00
|
|
|
if (LOGGING_ENABLED) console.error(err)
|
2023-04-11 20:43:37 +03:00
|
|
|
clearTimeout(timeout)
|
|
|
|
clearTimeout(hangTimeout)
|
2021-12-22 12:02:51 +03:00
|
|
|
const resp: Response<any> = {
|
|
|
|
id: request.id,
|
|
|
|
error: unknownError(err)
|
|
|
|
}
|
2021-12-06 18:16:38 +03:00
|
|
|
ws.send(serialize(resp))
|
|
|
|
}
|
2021-08-03 22:02:59 +03:00
|
|
|
}
|
|
|
|
|
2021-08-04 19:17:01 +03:00
|
|
|
/**
|
|
|
|
* @public
|
|
|
|
* @param sessionFactory -
|
2021-08-04 12:10:22 +03:00
|
|
|
* @param port -
|
|
|
|
* @param host -
|
2021-08-03 22:02:59 +03:00
|
|
|
*/
|
2022-04-23 06:45:55 +03:00
|
|
|
export function start (
|
|
|
|
ctx: MeasureContext,
|
2023-02-15 06:14:20 +03:00
|
|
|
pipelineFactory: PipelineFactory,
|
2022-05-23 18:53:33 +03:00
|
|
|
sessionFactory: (token: Token, pipeline: Pipeline, broadcast: BroadcastCall) => Session,
|
2022-04-23 06:45:55 +03:00
|
|
|
port: number,
|
2022-11-16 18:03:03 +03:00
|
|
|
productId: string,
|
2022-04-23 06:45:55 +03:00
|
|
|
host?: string
|
2023-01-04 20:58:54 +03:00
|
|
|
): () => Promise<void> {
|
2023-04-13 20:39:20 +03:00
|
|
|
if (LOGGING_ENABLED) console.log(`starting server on port ${port} ...`)
|
2021-08-03 22:02:59 +03:00
|
|
|
|
2022-05-23 18:53:33 +03:00
|
|
|
const sessions = new SessionManager(sessionFactory)
|
|
|
|
|
2022-11-02 11:50:14 +03:00
|
|
|
const wss = new WebSocketServer({
|
2022-05-23 18:53:33 +03:00
|
|
|
noServer: true,
|
|
|
|
perMessageDeflate: {
|
|
|
|
zlibDeflateOptions: {
|
|
|
|
// See zlib defaults.
|
2023-01-12 22:56:35 +03:00
|
|
|
chunkSize: 10 * 1024,
|
2022-05-23 18:53:33 +03:00
|
|
|
memLevel: 7,
|
|
|
|
level: 3
|
|
|
|
},
|
|
|
|
zlibInflateOptions: {
|
|
|
|
chunkSize: 10 * 1024
|
2023-01-12 22:56:35 +03:00
|
|
|
}
|
2022-05-23 18:53:33 +03:00
|
|
|
}
|
|
|
|
})
|
2021-08-03 22:02:59 +03:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
2023-02-28 10:02:15 +03:00
|
|
|
wss.on('connection', async (ws: WebSocket, request: any, token: Token, sessionId?: string) => {
|
2023-01-04 20:58:54 +03:00
|
|
|
let buffer: string[] | undefined = []
|
2021-08-19 20:34:58 +03:00
|
|
|
|
2022-04-23 06:45:55 +03:00
|
|
|
ws.on('message', (msg: string) => {
|
2023-01-04 20:58:54 +03:00
|
|
|
buffer?.push(msg)
|
2022-04-23 06:45:55 +03:00
|
|
|
})
|
2023-02-28 10:02:15 +03:00
|
|
|
const session = await sessions.addSession(ctx, ws, token, pipelineFactory, productId, sessionId)
|
2021-08-03 22:02:59 +03:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
2023-04-14 20:18:23 +03:00
|
|
|
ws.on('message', (msg: RawData) => {
|
2023-03-24 07:53:12 +03:00
|
|
|
let msgStr = ''
|
|
|
|
if (typeof msg === 'string') {
|
|
|
|
msgStr = msg
|
|
|
|
} else if (msg instanceof Buffer) {
|
|
|
|
msgStr = msg.toString()
|
|
|
|
} else if (Array.isArray(msg)) {
|
|
|
|
msgStr = Buffer.concat(msg).toString()
|
|
|
|
}
|
2023-04-14 20:18:23 +03:00
|
|
|
void handleRequest(ctx, session, ws, msgStr, token.workspace.name)
|
2023-03-24 07:53:12 +03:00
|
|
|
})
|
2022-04-23 06:45:55 +03:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
2023-03-24 07:53:12 +03:00
|
|
|
ws.on('close', (code: number, reason: Buffer) => {
|
|
|
|
if (session.workspaceClosed ?? false) {
|
|
|
|
return
|
|
|
|
}
|
2023-02-28 10:02:15 +03:00
|
|
|
// remove session after 1seconds, give a time to reconnect.
|
2023-03-24 07:53:12 +03:00
|
|
|
if (code === 1000) {
|
2023-04-20 13:11:22 +03:00
|
|
|
if (LOGGING_ENABLED) console.log(token.workspace.name, `client "${token.email}" closed normally`)
|
2023-03-24 07:53:12 +03:00
|
|
|
void sessions.close(ctx, ws, token.workspace, code, reason.toString())
|
|
|
|
} else {
|
2023-04-13 20:39:20 +03:00
|
|
|
if (LOGGING_ENABLED) {
|
2023-04-20 13:11:22 +03:00
|
|
|
console.log(
|
|
|
|
token.workspace.name,
|
|
|
|
`client "${token.email}" closed abnormally, waiting reconnect`,
|
|
|
|
code,
|
|
|
|
reason.toString()
|
|
|
|
)
|
2023-04-13 20:39:20 +03:00
|
|
|
}
|
2023-03-24 07:53:12 +03:00
|
|
|
session.closeTimeout = setTimeout(() => {
|
2023-04-20 13:11:22 +03:00
|
|
|
if (LOGGING_ENABLED) console.log(token.workspace.name, `client "${token.email}" force closed`)
|
2023-03-24 07:53:12 +03:00
|
|
|
void sessions.close(ctx, ws, token.workspace, code, reason.toString())
|
|
|
|
}, 10000)
|
|
|
|
}
|
2022-11-16 18:03:03 +03:00
|
|
|
})
|
2023-01-04 20:58:54 +03:00
|
|
|
const b = buffer
|
|
|
|
buffer = undefined
|
|
|
|
for (const msg of b) {
|
2023-04-05 11:47:10 +03:00
|
|
|
await handleRequest(ctx, session, ws, msg, token.workspace.name)
|
2021-08-19 20:34:58 +03:00
|
|
|
}
|
2021-08-03 22:02:59 +03:00
|
|
|
})
|
|
|
|
|
|
|
|
const server = createServer()
|
2023-04-20 13:11:22 +03:00
|
|
|
|
|
|
|
server.on('request', (request: IncomingMessage, response: ServerResponse) => {
|
|
|
|
const url = new URL('http://localhost' + (request.url ?? ''))
|
|
|
|
|
|
|
|
const token = url.pathname.substring(1)
|
|
|
|
try {
|
|
|
|
const payload = decodeToken(token ?? '')
|
|
|
|
console.log(payload.workspace, 'statistics request')
|
|
|
|
|
|
|
|
response.writeHead(200, {
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
'Access-Control-Allow-Origin': '*',
|
|
|
|
'Access-Control-Allow-Methods': 'GET, OPTIONS',
|
|
|
|
'Access-Control-Allow-Headers': 'Content-Type'
|
|
|
|
})
|
|
|
|
const data = {
|
|
|
|
metrics: metricsAggregate((ctx as any).metrics),
|
|
|
|
activeSessions: {}
|
|
|
|
}
|
|
|
|
for (const [k, v] of sessions.workspaces) {
|
|
|
|
;(data.activeSessions as any)[k] = v.sessions.length
|
|
|
|
}
|
|
|
|
const json = JSON.stringify(data)
|
|
|
|
response.end(json)
|
|
|
|
} catch (err) {
|
|
|
|
response.writeHead(404, {})
|
|
|
|
response.end()
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
2021-08-07 08:58:28 +03:00
|
|
|
server.on('upgrade', (request: IncomingMessage, socket: any, head: Buffer) => {
|
2023-02-28 10:02:15 +03:00
|
|
|
const url = new URL('http://localhost' + (request.url ?? ''))
|
|
|
|
const token = url.pathname.substring(1)
|
|
|
|
|
2021-08-04 10:56:34 +03:00
|
|
|
try {
|
2022-01-27 11:53:09 +03:00
|
|
|
const payload = decodeToken(token ?? '')
|
2023-02-28 10:02:15 +03:00
|
|
|
const sessionId = url.searchParams.get('sessionId')
|
2023-04-20 13:11:22 +03:00
|
|
|
if (LOGGING_ENABLED) console.log(payload.workspace.name, 'client connected with payload', payload, sessionId)
|
2022-11-16 18:03:03 +03:00
|
|
|
|
2022-11-22 21:13:26 +03:00
|
|
|
if (payload.workspace.productId !== productId) {
|
2022-11-16 18:03:03 +03:00
|
|
|
throw new Error('Invalid workspace product')
|
|
|
|
}
|
|
|
|
|
2023-02-28 10:02:15 +03:00
|
|
|
wss.handleUpgrade(request, socket, head, (ws) => wss.emit('connection', ws, request, payload, sessionId))
|
2021-08-04 10:56:34 +03:00
|
|
|
} catch (err) {
|
2023-04-13 20:39:20 +03:00
|
|
|
if (LOGGING_ENABLED) console.error('invalid token', err)
|
2022-07-05 08:43:17 +03:00
|
|
|
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
|
|
const resp: Response<any> = {
|
|
|
|
id: -1,
|
|
|
|
error: UNAUTHORIZED,
|
|
|
|
result: 'hello'
|
|
|
|
}
|
|
|
|
ws.send(serialize(resp))
|
|
|
|
ws.onmessage = (msg) => {
|
|
|
|
const resp: Response<any> = {
|
|
|
|
error: UNAUTHORIZED
|
|
|
|
}
|
|
|
|
ws.send(serialize(resp))
|
|
|
|
}
|
|
|
|
})
|
2021-08-03 22:02:59 +03:00
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
server.listen(port, host)
|
2023-01-04 20:58:54 +03:00
|
|
|
return async () => {
|
2021-11-22 14:17:10 +03:00
|
|
|
server.close()
|
2023-01-04 20:58:54 +03:00
|
|
|
await sessions.closeWorkspaces(ctx)
|
2021-11-22 14:17:10 +03:00
|
|
|
}
|
2021-08-03 22:02:59 +03:00
|
|
|
}
|