From 2799e8bb54da8e906ebaf855dcdc4514722a21d8 Mon Sep 17 00:00:00 2001 From: Kristina Date: Wed, 25 Dec 2024 14:09:50 +0400 Subject: [PATCH] Init ws server (#6) * Init ws server --- .npmrc | 2 + .vscode/launch.json | 24 +++ bun.lockb | Bin 59816 -> 101408 bytes eslint.config.js | 2 +- packages/sdk-types/src/event.ts | 227 ++++++++++++++++++++ packages/sdk-types/src/index.ts | 2 + packages/sdk-types/src/ws.ts | 17 ++ packages/server/package.json | 27 +++ packages/server/src/config.ts | 29 +++ packages/server/src/eventProcessor.ts | 275 +++++++++++++++++++++++++ packages/server/src/index.ts | 6 + packages/server/src/main.ts | 141 +++++++++++++ packages/server/src/manager.ts | 179 ++++++++++++++++ packages/server/src/server/error.ts | 8 + packages/server/src/server/server.ts | 26 +++ packages/server/src/session.ts | 76 +++++++ packages/server/src/triggers.ts | 112 ++++++++++ packages/server/src/types.ts | 7 + packages/server/src/utils/account.ts | 23 +++ packages/server/src/utils/logger.ts | 24 +++ packages/server/src/utils/serialize.ts | 41 ++++ packages/server/tsconfig.json | 8 + 22 files changed, 1255 insertions(+), 1 deletion(-) create mode 100644 .npmrc create mode 100644 .vscode/launch.json create mode 100644 packages/sdk-types/src/event.ts create mode 100644 packages/sdk-types/src/ws.ts create mode 100644 packages/server/package.json create mode 100644 packages/server/src/config.ts create mode 100644 packages/server/src/eventProcessor.ts create mode 100644 packages/server/src/index.ts create mode 100644 packages/server/src/main.ts create mode 100644 packages/server/src/manager.ts create mode 100644 packages/server/src/server/error.ts create mode 100644 packages/server/src/server/server.ts create mode 100644 packages/server/src/session.ts create mode 100644 packages/server/src/triggers.ts create mode 100644 packages/server/src/types.ts create mode 100644 packages/server/src/utils/account.ts create mode 100644 packages/server/src/utils/logger.ts create mode 100644 packages/server/src/utils/serialize.ts create mode 100644 packages/server/tsconfig.json diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000000..07f82b7293 --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +@hcengineering:registry=https://npm.pkg.github.com/ +//npm.pkg.github.com/:_authToken=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 \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000000..65434e460a --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,24 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Debug Server", + "env": { + "DB_URL": "postgresql://root@127.0.0.1:26257/my_database?sslmode=disable", + "ACCOUNTS_URL": "http://localhost:3000", + "SECRET": "secret" + }, + "runtimeExecutable": "bun", + "runtimeArgs": ["run"], + "args": ["src/index.ts"], + "cwd": "${workspaceFolder}/packages/server", + "protocol": "inspector", + "runtimeVersion": "20", + "showAsyncStacks": true, + "outputCapture": "std", + "sourceMaps": true + } + ] +} diff --git a/bun.lockb b/bun.lockb index 76fa802047aa892f2968c867da494978ab0cb830..bcfd9769cc9d102bab1d9e18fd386925410165fd 100755 GIT binary patch delta 34910 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 K3SQs4_WuD$ybT`! delta 8309 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 diff --git a/eslint.config.js b/eslint.config.js index 0a7a95e275..ab02536846 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -5,10 +5,10 @@ import configPrettier from "eslint-config-prettier"; /** @type {import('eslint').Linter.Config[]} */ export default [ - {files: ["**/*.ts"]}, pluginJs.configs.recommended, prettierRecommended, configPrettier, ...tseslint.configs.recommended, + {files: ["**/*.ts"], rules: {"@typescript-eslint/no-explicit-any": "off", '@typescript-eslint/no-empty-object-type': 'off'}}, ]; diff --git a/packages/sdk-types/src/event.ts b/packages/sdk-types/src/event.ts new file mode 100644 index 0000000000..00f49b6714 --- /dev/null +++ b/packages/sdk-types/src/event.ts @@ -0,0 +1,227 @@ +import type { + Attachment, + CardID, + ContextID, + Message, + MessageID, + NotificationContext, + NotificationContextUpdate, + Patch, + Reaction, + RichText, + SocialID, + Notification +} from '@communication/types' + +export enum EventType { + CreateMessage = 'createMessage', + RemoveMessage = 'removeMessage', + CreatePatch = 'createPatch', + CreateReaction = 'createReaction', + RemoveReaction = 'removeReaction', + CreateAttachment = 'createAttachment', + RemoveAttachment = 'removeAttachment', + CreateNotification = 'createNotification', + RemoveNotification = 'removeNotification', + CreateNotificationContext = 'createNotificationContext', + RemoveNotificationContext = 'removeNotificationContext', + UpdateNotificationContext = 'updateNotificationContext', + + MessageCreated = 'messageCreated', + MessageRemoved = 'messageRemoved', + PatchCreated = 'patchCreated', + ReactionCreated = 'reactionCreated', + ReactionRemoved = 'reactionRemoved', + AttachmentCreated = 'attachmentCreated', + AttachmentRemoved = 'attachmentRemoved', + NotificationCreated = 'notificationCreated', + NotificationRemoved = 'notificationRemoved', + NotificationContextCreated = 'notificationContextCreated', + NotificationContextRemoved = 'notificationContextRemoved', + NotificationContextUpdated = 'notificationContextUpdated' +} + +export type Event = + | CreateMessageEvent + | RemoveMessageEvent + | CreatePatchEvent + | CreateReactionEvent + | RemoveReactionEvent + | CreateAttachmentEvent + | RemoveAttachmentEvent + | CreateNotificationEvent + | RemoveNotificationEvent + | CreateNotificationContextEvent + | RemoveNotificationContextEvent + | UpdateNotificationContextEvent + +export interface CreateMessageEvent { + type: EventType.CreateMessage + card: CardID + content: RichText + creator: SocialID +} + +export interface RemoveMessageEvent { + type: EventType.RemoveMessage + message: MessageID +} + +export interface CreatePatchEvent { + type: EventType.CreatePatch + message: MessageID + content: RichText + creator: SocialID +} + +export interface CreateReactionEvent { + type: EventType.CreateReaction + message: MessageID + reaction: string + creator: SocialID +} + +export interface RemoveReactionEvent { + type: EventType.RemoveReaction + message: MessageID + reaction: string + creator: SocialID +} + +export interface CreateAttachmentEvent { + type: EventType.CreateAttachment + message: MessageID + card: CardID + creator: SocialID +} + +export interface RemoveAttachmentEvent { + type: EventType.RemoveAttachment + message: MessageID + card: CardID +} + +export interface CreateNotificationEvent { + type: EventType.CreateNotification + message: MessageID + context: ContextID +} + +export interface RemoveNotificationEvent { + type: EventType.RemoveNotification + message: MessageID + context: ContextID +} + +export interface CreateNotificationContextEvent { + type: EventType.CreateNotificationContext + card: CardID + lastView?: Date + lastUpdate?: Date +} + +export interface RemoveNotificationContextEvent { + type: EventType.RemoveNotificationContext + context: ContextID +} + +export interface UpdateNotificationContextEvent { + type: EventType.UpdateNotificationContext + context: ContextID + update: NotificationContextUpdate +} + +export type EventResult = CreateMessageResult | CreateNotificationContextResult | {} + +export interface CreateMessageResult { + id: MessageID +} + +export interface CreateNotificationContextResult { + id: ContextID +} + +//TODO: THINK ABOUT BETTER NAMES +export type BroadcastEvent = + | MessageCreatedEvent + | MessageRemovedEvent + | PatchCreatedEvent + | ReactionCreatedEvent + | ReactionRemovedEvent + | AttachmentCreatedEvent + | AttachmentRemovedEvent + | NotificationCreatedEvent + | NotificationRemovedEvent + | NotificationContextCreatedEvent + | NotificationContextRemovedEvent + | NotificationContextUpdatedEvent + +export interface MessageCreatedEvent { + type: EventType.MessageCreated + card: CardID + message: Message +} + +export interface MessageRemovedEvent { + type: EventType.MessageRemoved + message: MessageID +} + +export interface PatchCreatedEvent { + type: EventType.PatchCreated + patch: Patch +} + +export interface ReactionCreatedEvent { + type: EventType.ReactionCreated + reaction: Reaction +} + +export interface ReactionRemovedEvent { + type: EventType.ReactionRemoved + message: MessageID + reaction: string + creator: SocialID +} + +export interface AttachmentCreatedEvent { + type: EventType.AttachmentCreated + attachment: Attachment +} + +export interface AttachmentRemovedEvent { + type: EventType.AttachmentRemoved + message: MessageID + card: CardID +} + +export interface NotificationCreatedEvent { + type: EventType.NotificationCreated + personWorkspace: string + notification: Notification +} + +export interface NotificationRemovedEvent { + type: EventType.NotificationRemoved + personWorkspace: string + message: MessageID + context: ContextID +} + +export interface NotificationContextCreatedEvent { + type: EventType.NotificationContextCreated + context: NotificationContext +} + +export interface NotificationContextRemovedEvent { + type: EventType.NotificationContextRemoved + personWorkspace: string + context: ContextID +} + +export interface NotificationContextUpdatedEvent { + type: EventType.NotificationContextUpdated + personWorkspace: string + context: ContextID + update: NotificationContextUpdate +} diff --git a/packages/sdk-types/src/index.ts b/packages/sdk-types/src/index.ts index 1beb455f5e..084f5c7e90 100644 --- a/packages/sdk-types/src/index.ts +++ b/packages/sdk-types/src/index.ts @@ -1 +1,3 @@ export * from './db' +export * from './event' +export * from './ws' diff --git a/packages/sdk-types/src/ws.ts b/packages/sdk-types/src/ws.ts new file mode 100644 index 0000000000..ed91b811b7 --- /dev/null +++ b/packages/sdk-types/src/ws.ts @@ -0,0 +1,17 @@ +export type RequestId = string + +export interface Response { + id?: RequestId + result?: any + error?: string //TODO: Use platform error +} + +export interface Request { + id?: RequestId + method: string + params: any[] +} + +export interface HelloRequest extends Request { + binary?: boolean +} diff --git a/packages/server/package.json b/packages/server/package.json new file mode 100644 index 0000000000..43c5e0cd85 --- /dev/null +++ b/packages/server/package.json @@ -0,0 +1,27 @@ +{ + "name": "@communication/server", + "version": "0.1.0", + "main": "src/index.ts", + "module": "src/index.ts", + "type": "module", + "devDependencies": { + "@types/bun": "^1.1.14", + "@types/express": "^5.0.0", + "@types/cors": "^2.8.17", + "@types/ws": "^8.5.13" + }, + "dependencies": { + "@hcengineering/server-token": "^0.6.377", + "@communication/cockroach": "workspace:*", + "@communication/sdk-types": "workspace:*", + "@communication/types": "workspace:*", + "cors": "^2.8.5", + "dotenv": "^16.4.7", + "express": "^4.21.2", + "msgpackr": "^1.11.2", + "ws": "^8.18.0" + }, + "peerDependencies": { + "typescript": "^5.6.3" + } +} diff --git a/packages/server/src/config.ts b/packages/server/src/config.ts new file mode 100644 index 0000000000..2f835bc72a --- /dev/null +++ b/packages/server/src/config.ts @@ -0,0 +1,29 @@ +interface Config { + Port: number + DbUrl: string + AccountsUrl: string + Secret: string +} + +const parseNumber = (str: string | undefined): number | undefined => (str !== undefined ? Number(str) : undefined) + +const config: Config = (() => { + const params: Partial = { + Port: parseNumber(process.env.PORT) ?? 8090, + DbUrl: process.env.DB_URL, + AccountsUrl: process.env.ACCOUNTS_URL, + Secret: process.env.SECRET + } + + const missingEnv = Object.entries(params) + .filter(([, value]) => value === undefined) + .map(([key]) => key) + + if (missingEnv.length > 0) { + throw Error(`Missing env variables: ${missingEnv.join(', ')}`) + } + + return params as Config +})() + +export default config diff --git a/packages/server/src/eventProcessor.ts b/packages/server/src/eventProcessor.ts new file mode 100644 index 0000000000..ecdb2d9a5d --- /dev/null +++ b/packages/server/src/eventProcessor.ts @@ -0,0 +1,275 @@ +import { type Message, type Patch, type Reaction, type Attachment } from '@communication/types' +import { + EventType, + type CreateAttachmentEvent, + type AttachmentCreatedEvent, + type CreateMessageEvent, + type MessageCreatedEvent, + type CreatePatchEvent, + type PatchCreatedEvent, + type CreateReactionEvent, + type ReactionCreatedEvent, + type Event, + type BroadcastEvent, + type RemoveAttachmentEvent, + type AttachmentRemovedEvent, + type RemoveMessageEvent, + type MessageRemovedEvent, + type RemoveReactionEvent, + type ReactionRemovedEvent, + type EventResult, + type DbAdapter, + type CreateNotificationEvent, + type RemoveNotificationEvent, + type CreateNotificationContextEvent, + type RemoveNotificationContextEvent, + type UpdateNotificationContextEvent, + type NotificationRemovedEvent, + type NotificationContextCreatedEvent, + type NotificationContextRemovedEvent, + type NotificationContextUpdatedEvent +} from '@communication/sdk-types' + +type Result = { + broadcastEvent?: BroadcastEvent + result: EventResult +} + +export class EventProcessor { + constructor( + private readonly db: DbAdapter, + private readonly workspace: string, + private readonly personWorkspace: string + ) {} + + async process(event: Event): Promise { + switch (event.type) { + case EventType.CreateMessage: + return await this.createMessage(event) + case EventType.RemoveMessage: + return await this.removeMessage(event) + case EventType.CreatePatch: + return await this.createPatch(event) + case EventType.CreateReaction: + return await this.createReaction(event) + case EventType.RemoveReaction: + return await this.removeReaction(event) + case EventType.CreateAttachment: + return await this.createAttachment(event) + case EventType.RemoveAttachment: + return await this.removeAttachment(event) + case EventType.CreateNotification: + return await this.createNotification(event) + case EventType.RemoveNotification: + return await this.removeNotification(event) + case EventType.CreateNotificationContext: + return await this.createNotificationContext(event) + case EventType.RemoveNotificationContext: + return await this.removeNotificationContext(event) + case EventType.UpdateNotificationContext: + return await this.updateNotificationContext(event) + } + } + + private async createMessage(event: CreateMessageEvent): Promise { + const created = new Date() + const id = await this.db.createMessage(event.content, event.creator, created) + await this.db.placeMessage(id, event.card, this.workspace) + const message: Message = { + id, + content: event.content, + creator: event.creator, + created: created, + edited: created, + reactions: [], + attachments: [] + } + const broadcastEvent: MessageCreatedEvent = { + type: EventType.MessageCreated, + card: event.card, + message + } + return { + broadcastEvent, + result: { id } + } + } + + private async createPatch(event: CreatePatchEvent): Promise { + const created = new Date() + await this.db.createPatch(event.message, event.content, event.creator, created) + + const patch: Patch = { + message: event.message, + content: event.content, + creator: event.creator, + created: created + } + const broadcastEvent: PatchCreatedEvent = { + type: EventType.PatchCreated, + patch + } + return { + broadcastEvent, + result: {} + } + } + + private async removeMessage(event: RemoveMessageEvent): Promise { + await this.db.removeMessage(event.message) + + const broadcastEvent: MessageRemovedEvent = { + type: EventType.MessageRemoved, + message: event.message + } + + return { + broadcastEvent, + result: {} + } + } + + private async createReaction(event: CreateReactionEvent): Promise { + const created = new Date() + await this.db.createReaction(event.message, event.reaction, event.creator, created) + + const reaction: Reaction = { + message: event.message, + reaction: event.reaction, + creator: event.creator, + created: created + } + const broadcastEvent: ReactionCreatedEvent = { + type: EventType.ReactionCreated, + reaction + } + return { + broadcastEvent, + result: {} + } + } + + private async removeReaction(event: RemoveReactionEvent): Promise { + await this.db.removeReaction(event.message, event.reaction, event.creator) + const broadcastEvent: ReactionRemovedEvent = { + type: EventType.ReactionRemoved, + message: event.message, + reaction: event.reaction, + creator: event.creator + } + return { + broadcastEvent, + result: {} + } + } + + private async createAttachment(event: CreateAttachmentEvent): Promise { + const created = new Date() + await this.db.createAttachment(event.message, event.card, event.creator, created) + + const attachment: Attachment = { + message: event.message, + card: event.card, + creator: event.creator, + created: created + } + const broadcastEvent: AttachmentCreatedEvent = { + type: EventType.AttachmentCreated, + attachment + } + + return { + broadcastEvent, + result: {} + } + } + + private async removeAttachment(event: RemoveAttachmentEvent): Promise { + await this.db.removeAttachment(event.message, event.card) + const broadcastEvent: AttachmentRemovedEvent = { + type: EventType.AttachmentRemoved, + message: event.message, + card: event.card + } + return { + broadcastEvent, + result: {} + } + } + + private async createNotification(event: CreateNotificationEvent): Promise { + await this.db.createNotification(event.message, event.context) + + return { + result: {} + } + } + + private async removeNotification(event: RemoveNotificationEvent): Promise { + await this.db.removeNotification(event.message, event.context) + + const broadcastEvent: NotificationRemovedEvent = { + type: EventType.NotificationRemoved, + personWorkspace: this.personWorkspace, + message: event.message, + context: event.context + } + return { + broadcastEvent, + result: {} + } + } + + private async createNotificationContext(event: CreateNotificationContextEvent): Promise { + const id = await this.db.createContext( + this.workspace, + event.card, + this.personWorkspace, + event.lastView, + event.lastUpdate + ) + const broadcastEvent: NotificationContextCreatedEvent = { + type: EventType.NotificationContextCreated, + context: { + id, + workspace: this.workspace, + personWorkspace: this.personWorkspace, + card: event.card, + lastView: event.lastView, + lastUpdate: event.lastUpdate + } + } + return { + broadcastEvent, + result: { id } + } + } + + private async removeNotificationContext(event: RemoveNotificationContextEvent): Promise { + await this.db.removeContext(event.context) + const broadcastEvent: NotificationContextRemovedEvent = { + type: EventType.NotificationContextRemoved, + personWorkspace: this.personWorkspace, + context: event.context + } + return { + broadcastEvent, + result: {} + } + } + + async updateNotificationContext(event: UpdateNotificationContextEvent): Promise { + await this.db.updateContext(event.context, event.update) + + const broadcastEvent: NotificationContextUpdatedEvent = { + type: EventType.NotificationContextUpdated, + personWorkspace: this.personWorkspace, + context: event.context, + update: event.update + } + return { + broadcastEvent, + result: {} + } + } +} diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts new file mode 100644 index 0000000000..f959c1eb96 --- /dev/null +++ b/packages/server/src/index.ts @@ -0,0 +1,6 @@ +import { config } from 'dotenv' + +import { main } from './main.ts' + +config() +void main() diff --git a/packages/server/src/main.ts b/packages/server/src/main.ts new file mode 100644 index 0000000000..8d22d60e43 --- /dev/null +++ b/packages/server/src/main.ts @@ -0,0 +1,141 @@ +import WebSocket, { WebSocketServer, type RawData } from 'ws' +import { createDbAdapter } from '@communication/cockroach' +import type { Response, HelloRequest } from '@communication/sdk-types' +import { decodeToken } from '@hcengineering/server-token' + +import type { ConnectionInfo } from './types.ts' +import { deserializeRequest, serializeResponse } from './utils/serialize.ts' +import config from './config' +import { listen, createServer } from './server/server' +import { ConsoleLogger } from './utils/logger' +import { Manager } from './manager.ts' +import type { Session } from './session.ts' +import { getWorkspaceInfo } from './utils/account.ts' + +const logger = new ConsoleLogger() + +const pingTimeout = 10000 +const requestTimeout = 60 * 1000 + +//TODO: use platform errors +const UNAUTHORIZED_ERROR = 'Unauthorized' +const UNKNOWN_ERROR = 'Unknown' + +export const main = async (): Promise => { + const server = listen(createServer(), config.Port) + const wss = new WebSocketServer({ noServer: true }) + const db = await createDbAdapter(config.DbUrl) + const manager = new Manager(db) + + server.on('upgrade', async (req, socket, head) => { + const url = new URL('http://localhost' + (req.url ?? '')) + const token = url.searchParams.get('token') ?? '' + + try { + const info = await validateToken(token) + wss.handleUpgrade(req, socket, head, (ws) => { + handleConnection(ws, manager, info) + }) + } catch (error: any) { + logger.error('Invalid token', { error }) + wss.handleUpgrade(req, socket, head, (ws) => { + const resp: Response = { + result: UNAUTHORIZED_ERROR, + error + } + sendResponse(ws, resp, false) + socket.destroy() + }) + } + }) + + const shutdown = (): void => { + db.close() + server.close(() => { + process.exit() + }) + } + + process.on('SIGINT', shutdown) + process.on('SIGTERM', shutdown) + process.on('uncaughtException', (e) => { + console.error(e) + }) + process.on('unhandledRejection', (e) => { + console.error(e) + }) +} + +function handleConnection(ws: WebSocket, manager: Manager, info: ConnectionInfo) { + const session = manager.createSession(ws, info) + const pingInterval = setInterval(() => { + const now = Date.now() + const lastRequestDiff = now - session.lastRequest + + if (lastRequestDiff > requestTimeout) { + console.warn(`Connection inactive for ${lastRequestDiff}ms, closing`, info) + manager.closeSession(ws, info.workspace) + ws.close() + clearInterval(pingInterval) + return + } + sendResponse(ws, { id: 'ping', result: 'ping' }, session.binary) + }, pingTimeout) + + ws.on('message', async (raw: RawData) => { + try { + await handleRequest(raw, session, ws) + } catch (err: any) { + logger.error('Error during message handling', { err }) + } + }) + + ws.on('close', () => { + manager.closeSession(ws, info.workspace) + clearInterval(pingInterval) + }) + + ws.on('error', (error) => { + logger.log('Error', { error, ...info }) + }) +} + +function sendResponse(ws: WebSocket, resp: Response, binary: boolean) { + ws.send(serializeResponse(resp, binary), { binary }) +} + +async function handleRequest(raw: RawData, session: Session, ws: WebSocket) { + const request = deserializeRequest(raw, session.binary) + if (request === undefined) return + + if (request.id === 'hello') { + const hello = request as HelloRequest + session.binary = hello.binary ?? false + sendResponse(ws, { id: 'hello', result: 'hello' }, false) + return + } + + try { + const fn = (session as any)[request.method] + const params = [...request.params] + const result = await fn.apply(session, params) + const response: Response = { id: request.id, result } + sendResponse(ws, response, session.binary) + } catch (err: any) { + const response: Response = { id: request.id, result: UNKNOWN_ERROR, error: err } + sendResponse(ws, response, session.binary) + } +} + +//TODO: decodeToken or authorize with account service or both +async function validateToken(token: string): Promise { + const { email } = decodeToken(token, true, config.Secret) + const info = await getWorkspaceInfo(token) + + if (info === undefined) { + throw new Error('No workspace info') + } + + const personWorkspace = 'cd0aba36-1c4f-4170-95f2-27a12a5415f7' + return { workspace: info.workspaceId, personWorkspace, socialId: email } +} diff --git a/packages/server/src/manager.ts b/packages/server/src/manager.ts new file mode 100644 index 0000000000..3bb640cc36 --- /dev/null +++ b/packages/server/src/manager.ts @@ -0,0 +1,179 @@ +import WebSocket from 'ws' +import { + type BroadcastEvent, + type DbAdapter, + EventType, + type MessageCreatedEvent, + type NotificationContextCreatedEvent, + type NotificationCreatedEvent, + type Response +} from '@communication/sdk-types' +import type { FindMessagesParams, FindNotificationContextParams, FindNotificationsParams } from '@communication/types' + +import { Session } from './session' +import type { ConnectionInfo } from './types' +import { serializeResponse } from './utils/serialize.ts' +import { Triggers } from './triggers.ts' + +type QueryId = number | string +type QueryType = 'message' | 'notification' | 'context' + +type SessionInfo = { + session: Session + ws: WebSocket + messageQueries: Map + notificationQueries: Map + contextQueries: Map +} + +export class Manager { + private sessionsByWorkspace: Map = new Map() + private triggers: Triggers + private lastSessionId: number = 0 + + constructor(private readonly db: DbAdapter) { + this.triggers = new Triggers(db) + } + + createSession(ws: WebSocket, info: ConnectionInfo): Session { + const current = this.sessionsByWorkspace.get(info.workspace) ?? [] + this.lastSessionId++ + const session = new Session(this.lastSessionId, info, this.db, this) + current.push({ session, ws, messageQueries: new Map(), notificationQueries: new Map(), contextQueries: new Map() }) + this.sessionsByWorkspace.set(info.workspace, current) + return session + } + + closeSession(ws: WebSocket, workspace: string): void { + const sessions = this.sessionsByWorkspace.get(workspace) ?? [] + if (sessions.length === 0) return + const newSessions = sessions.filter((it) => it.ws !== ws) + if (newSessions.length === 0) { + this.sessionsByWorkspace.delete(workspace) + } else { + this.sessionsByWorkspace.set(workspace, newSessions) + } + } + + getSessionInfo(sessionId: number, workspace: string): SessionInfo | undefined { + const sessions = this.sessionsByWorkspace.get(workspace) ?? [] + return sessions.find((it) => it.session.id === sessionId) + } + + subscribeQuery( + sessionId: number, + workspace: string, + type: QueryType, + queryId: number, + params: Record + ): void { + const info = this.getSessionInfo(sessionId, workspace) + if (info == null) return + + if (type === 'message') { + info.messageQueries.set(queryId, params) + } else if (type === 'notification') { + info.notificationQueries.set(queryId, params) + } else if (type === 'context') { + info.contextQueries.set(queryId, params) + } + } + + unsubscribeQuery(sessionId: number, workspace: string, queryId: number): void { + const info = this.getSessionInfo(sessionId, workspace) + if (info == null) return + + info.messageQueries.delete(queryId) + info.notificationQueries.delete(queryId) + info.contextQueries.delete(queryId) + } + + async next(event: BroadcastEvent, workspace: string): Promise { + await this.broadcast(event, workspace) + const derived = await this.triggers.process(event, workspace) + const derivedPromises: Promise[] = [] + for (const d of derived) { + derivedPromises.push(this.next(d, workspace)) + } + await Promise.all(derivedPromises) + } + + private async broadcast(event: BroadcastEvent, workspace: string): Promise { + const sessions = this.sessionsByWorkspace.get(workspace) ?? [] + const response: Response = { result: event } + for (const session of sessions) { + const msg = serializeResponse(response, session.session.binary) + if (this.match(event, session)) { + session.ws.send(msg) + } + } + } + + private match(event: BroadcastEvent, info: SessionInfo): boolean { + switch (event.type) { + case EventType.MessageCreated: + return this.matchMessagesQuery(event, Array.from(info.messageQueries.values())) + case EventType.PatchCreated: + case EventType.MessageRemoved: + case EventType.ReactionCreated: + case EventType.ReactionRemoved: + case EventType.AttachmentCreated: + case EventType.AttachmentRemoved: + return info.messageQueries.size > 0 + case EventType.NotificationCreated: + return ( + info.session.info.personWorkspace === event.personWorkspace && + this.matchNotificationQuery(event, Array.from(info.notificationQueries.values())) + ) + case EventType.NotificationRemoved: + return info.session.info.personWorkspace === event.personWorkspace && info.notificationQueries.size > 0 + case EventType.NotificationContextCreated: + return ( + info.session.info.personWorkspace === event.context.personWorkspace && + this.matchContextQuery(event, Array.from(info.contextQueries.values())) + ) + case EventType.NotificationContextRemoved: + return info.session.info.personWorkspace === event.personWorkspace && info.contextQueries.size > 0 + case EventType.NotificationContextUpdated: + return info.session.info.personWorkspace === event.personWorkspace && info.contextQueries.size > 0 + } + } + + private matchMessagesQuery(event: MessageCreatedEvent, queries: FindMessagesParams[]): boolean { + if (queries.length === 0) return false + + for (const query of queries) { + if (query.id != null && query.id !== event.message.id) continue + if (query.card != null && query.card !== event.card) continue + return true + } + + return false + } + + private matchNotificationQuery(event: NotificationCreatedEvent, queries: FindNotificationsParams[]): boolean { + if (queries.length === 0) return false + + for (const query of queries) { + if (query.context != null && query.context !== event.notification.context) continue + if (query.message != null && query.message !== event.notification.message.id) continue + if (query.read != null && query.read !== event.notification.read) continue + if (query.archived != null && query.archived !== event.notification.archived) continue + return true + } + + return false + } + + private matchContextQuery(event: NotificationContextCreatedEvent, queries: FindNotificationContextParams[]): boolean { + if (queries.length === 0) return false + + for (const query of queries) { + if (query.id != null && query.id !== event.context.id) continue + if (query.card != null && query.card !== event.context.card) continue + return true + } + + return false + } +} diff --git a/packages/server/src/server/error.ts b/packages/server/src/server/error.ts new file mode 100644 index 0000000000..55af5f0946 --- /dev/null +++ b/packages/server/src/server/error.ts @@ -0,0 +1,8 @@ +export class ApiError extends Error { + constructor ( + readonly code: string, + readonly message: string + ) { + super(message) + } +} \ No newline at end of file diff --git a/packages/server/src/server/server.ts b/packages/server/src/server/server.ts new file mode 100644 index 0000000000..0941596c97 --- /dev/null +++ b/packages/server/src/server/server.ts @@ -0,0 +1,26 @@ +import cors from 'cors' +import express, { type Express } from 'express' +import { Server } from 'http' + +import { ApiError } from './error' + +export function createServer (): Express { + const app = express() + + app.use(cors()) + app.use(express.json()) + + app.use((_req, res, _next) => { + res.status(404).send({ message: 'Not found' }) + }) + + return app +} + +export function listen (e: Express, port: number, host?: string): Server { + const cb = (): void => { + console.log(`Communication server has been started at ${host ?? '*'}:${port}`) + } + + return host !== undefined ? e.listen(port, host, cb) : e.listen(port, cb) +} diff --git a/packages/server/src/session.ts b/packages/server/src/session.ts new file mode 100644 index 0000000000..3efd0a1a86 --- /dev/null +++ b/packages/server/src/session.ts @@ -0,0 +1,76 @@ +import type { DbAdapter, Event, EventResult } from '@communication/sdk-types' +import type { + FindMessagesParams, + FindNotificationContextParams, + FindNotificationsParams, + Message, + Notification, + NotificationContext +} from '@communication/types' + +import type { ConnectionInfo } from './types' +import { EventProcessor } from './eventProcessor.ts' +import type { Manager } from './manager.ts' + +export class Session { + binary: boolean = false + lastRequest: number = Date.now() + + private readonly eventProcessor: EventProcessor + + constructor( + readonly id: number, + readonly info: ConnectionInfo, + private readonly db: DbAdapter, + private readonly manager: Manager + ) { + this.eventProcessor = new EventProcessor(db, info.workspace, info.personWorkspace) + } + + ping(): string { + this.lastRequest = Date.now() + return 'pong' + } + + async findMessages(params: FindMessagesParams, queryId?: number): Promise { + const result = await this.db.findMessages(this.info.workspace, params) + if (queryId != null) { + this.manager.subscribeQuery(this.id, this.info.workspace, 'message', queryId, params) + } + return result + } + + async unsubscribeQuery(id: number): Promise { + this.manager.unsubscribeQuery(this.id, this.info.workspace, id) + } + + async findNotifications(params: FindNotificationsParams, queryId?: number): Promise { + //TODO: do we need filter by workspace by default? + const result = await this.db.findNotifications(params, this.info.personWorkspace) + if (queryId != null) { + this.manager.subscribeQuery(this.id, this.info.workspace, 'notification', queryId, params) + } + return result + } + + async findNotificationContexts( + params: FindNotificationContextParams, + queryId?: number + ): Promise { + //TODO: do we need filter by workspace by default? + const result = await this.db.findContexts(params, [this.info.personWorkspace]) + if (queryId != null) { + this.manager.subscribeQuery(this.id, this.info.workspace, 'context', queryId, params) + } + + return result + } + + async event(event: Event): Promise { + const { result, broadcastEvent } = await this.eventProcessor.process(event) + if (broadcastEvent !== undefined) { + void this.manager.next(broadcastEvent, this.info.workspace) + } + return result + } +} diff --git a/packages/server/src/triggers.ts b/packages/server/src/triggers.ts new file mode 100644 index 0000000000..4f131853c6 --- /dev/null +++ b/packages/server/src/triggers.ts @@ -0,0 +1,112 @@ +import { + type BroadcastEvent, + type DbAdapter, + EventType, + type MessageCreatedEvent, + type NotificationContextCreatedEvent, + type NotificationCreatedEvent +} from '@communication/sdk-types' +import type { NotificationContext, ContextID } from '@communication/types' + +export class Triggers { + constructor(private readonly db: DbAdapter) {} + + async process(event: BroadcastEvent, workspace: string): Promise { + switch (event.type) { + case EventType.MessageCreated: + return this.createNotifications(event, workspace) + } + + return [] + } + + private async createNotifications(event: MessageCreatedEvent, workspace: string): Promise { + const card = event.card + const subscribedPersonWorkspaces = ['cd0aba36-1c4f-4170-95f2-27a12a5415f7', 'cd0aba36-1c4f-4170-95f2-27a12a5415f8'] + + const res: BroadcastEvent[] = [] + const contexts = await this.db.findContexts({ card }, [], workspace) + + res.push(...(await this.updateNotificationContexts(event.message.created, contexts))) + + for (const personWorkspace of subscribedPersonWorkspaces) { + const existsContext = contexts.find( + (it) => it.card === card && it.personWorkspace === personWorkspace && workspace === it.workspace + ) + const contextId = await this.getOrCreateContextId( + workspace, + card, + personWorkspace, + res, + event.message.created, + existsContext + ) + + await this.db.createNotification(event.message.id, contextId) + + const resultEvent: NotificationCreatedEvent = { + type: EventType.NotificationCreated, + personWorkspace, + notification: { + context: contextId, + message: event.message, + read: false, + archived: false + } + } + res.push(resultEvent) + } + + return res + } + + private async getOrCreateContextId( + workspace: string, + card: string, + personWorkspace: string, + res: BroadcastEvent[], + lastUpdate: Date, + context?: NotificationContext + ): Promise { + if (context !== undefined) { + return context.id + } else { + const contextId = await this.db.createContext(workspace, card, personWorkspace, undefined, lastUpdate) + const newContext = { + id: contextId, + card, + workspace, + personWorkspace + } + const resultEvent: NotificationContextCreatedEvent = { + type: EventType.NotificationContextCreated, + context: newContext + } + + res.push(resultEvent) + + return contextId + } + } + + private async updateNotificationContexts( + lastUpdate: Date, + contexts: NotificationContext[] + ): Promise { + const res: BroadcastEvent[] = [] + for (const context of contexts) { + if (context.lastUpdate === undefined || context.lastUpdate < lastUpdate) { + await this.db.updateContext(context.id, { lastUpdate }) + res.push({ + type: EventType.NotificationContextUpdated, + personWorkspace: context.personWorkspace, + context: context.id, + update: { + lastUpdate + } + }) + } + } + return res + } +} diff --git a/packages/server/src/types.ts b/packages/server/src/types.ts new file mode 100644 index 0000000000..8d46b65816 --- /dev/null +++ b/packages/server/src/types.ts @@ -0,0 +1,7 @@ +import type { SocialID } from '@communication/types' + +export interface ConnectionInfo { + workspace: string + personWorkspace: string + socialId: SocialID +} diff --git a/packages/server/src/utils/account.ts b/packages/server/src/utils/account.ts new file mode 100644 index 0000000000..882e437f25 --- /dev/null +++ b/packages/server/src/utils/account.ts @@ -0,0 +1,23 @@ +import config from "../config.ts"; + + +type WorkspaceInfo = { +workspaceId: string +} + +export async function getWorkspaceInfo (token: string): Promise { + const accountsUrl = config.AccountsUrl + const response = await fetch(accountsUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer ' + token + }, + body: JSON.stringify({ + method: 'getWorkspaceInfo', + params: [] + }) + }) + const result = await response.json() + return result.result as WorkspaceInfo | undefined +} \ No newline at end of file diff --git a/packages/server/src/utils/logger.ts b/packages/server/src/utils/logger.ts new file mode 100644 index 0000000000..828c27e432 --- /dev/null +++ b/packages/server/src/utils/logger.ts @@ -0,0 +1,24 @@ +export interface Logger { + log: (message: string, data?: Record) => void + warn: (message: string, data?: Record) => void + error: (message: string, data?: Record) => void + debug: (message: string, data?: Record) => void +} + +export class ConsoleLogger implements Logger { + log (message: string, data?: Record): void { + console.log({ message, ...data }) + } + + warn (message: string, data?: Record): void { + console.warn({ message, ...data }) + } + + error (message: string, data?: Record): void { + console.error({ message, ...data }) + } + + debug (message: string, data?: Record): void { + console.debug({ message, ...data }) + } +} \ No newline at end of file diff --git a/packages/server/src/utils/serialize.ts b/packages/server/src/utils/serialize.ts new file mode 100644 index 0000000000..1aa35209da --- /dev/null +++ b/packages/server/src/utils/serialize.ts @@ -0,0 +1,41 @@ +import { Packr } from 'msgpackr' +import type {Response, Request} from '@communication/sdk-types' + +import type {RawData} from "ws"; + +const packr = new Packr({ structuredClone: true, bundleStrings: true, copyBuffers: false }) + +export function serializeResponse(resp: Response, binary: boolean) { + return binary ? serializeBinary(resp) : serializeJson(resp) +} + +export function deserializeRequest(raw: RawData, binary: boolean): Request | undefined { + let buff: Buffer | undefined + if (raw instanceof Buffer) { + buff = raw + } else if (Array.isArray(raw)) { + buff = Buffer.concat(raw.map(it => new Uint8Array(it))) + } + + if(buff === undefined) { + return undefined + } + + return binary ? deserializeBinary(buff) : deserializeJson(buff) +} + +function deserializeBinary(data: any): any { + return packr.decode(data) +} + +function deserializeJson(data: any): any { + return JSON.parse(data.toString()) +} + +function serializeBinary(data: any) { + return new Uint8Array(packr.encode(data)) +} + +function serializeJson(data: any) { + return JSON.stringify(data) +} \ No newline at end of file diff --git a/packages/server/tsconfig.json b/packages/server/tsconfig.json new file mode 100644 index 0000000000..49e05cea1e --- /dev/null +++ b/packages/server/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src"] +}