mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-13 05:07:43 +02:00
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "@communication/client-ws",
|
||||
"version": "0.1.0",
|
||||
"main": "src/index.ts",
|
||||
"module": "src/index.ts",
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.1.14"
|
||||
},
|
||||
"dependencies": {
|
||||
"@communication/types": "workspace:*",
|
||||
"@communication/sdk-types": "workspace:*",
|
||||
"@msgpack/msgpack": "^3.0.0-beta2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.6.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
import {
|
||||
type CardID,
|
||||
type ContextID,
|
||||
type FindMessagesParams,
|
||||
type FindNotificationContextParams,
|
||||
type FindNotificationsParams,
|
||||
type Message,
|
||||
type MessageID,
|
||||
type Notification,
|
||||
type NotificationContext,
|
||||
type NotificationContextUpdate,
|
||||
type RichText,
|
||||
type SocialID
|
||||
} from '@communication/types'
|
||||
import {
|
||||
type BroadcastEvent,
|
||||
type Client,
|
||||
type CreateAttachmentEvent,
|
||||
type CreateMessageEvent,
|
||||
type CreateMessageResult,
|
||||
type CreateNotificationContextEvent,
|
||||
type CreateNotificationContextResult,
|
||||
type CreateNotificationEvent,
|
||||
type CreatePatchEvent,
|
||||
type CreateReactionEvent,
|
||||
type Event,
|
||||
type EventResult,
|
||||
EventType,
|
||||
type RemoveAttachmentEvent,
|
||||
type RemoveMessageEvent,
|
||||
type RemoveNotificationContextEvent,
|
||||
type RemoveNotificationEvent,
|
||||
type RemoveReactionEvent,
|
||||
type UpdateNotificationContextEvent
|
||||
} from '@communication/sdk-types'
|
||||
|
||||
import { WebSocketConnection } from './connection'
|
||||
|
||||
class WsClient implements Client {
|
||||
private readonly ws: WebSocketConnection
|
||||
|
||||
onEvent: (event: BroadcastEvent) => void = () => {}
|
||||
|
||||
constructor(
|
||||
private readonly url: string,
|
||||
private readonly token: string,
|
||||
private readonly binary: boolean = false
|
||||
) {
|
||||
const connectionUrl = this.url + '?token=' + this.token
|
||||
this.ws = new WebSocketConnection(connectionUrl, this.binary)
|
||||
this.ws.onEvent = (event) => {
|
||||
void this.onEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
async createMessage(card: CardID, content: RichText, creator: SocialID): Promise<MessageID> {
|
||||
const event: CreateMessageEvent = {
|
||||
type: EventType.CreateMessage,
|
||||
card,
|
||||
content,
|
||||
creator
|
||||
}
|
||||
const result = await this.sendEvent(event)
|
||||
return (result as CreateMessageResult).id
|
||||
}
|
||||
|
||||
async removeMessage(message: MessageID) {
|
||||
const event: RemoveMessageEvent = {
|
||||
type: EventType.RemoveMessage,
|
||||
message
|
||||
}
|
||||
await this.sendEvent(event)
|
||||
}
|
||||
|
||||
async createPatch(message: MessageID, content: RichText, creator: SocialID): Promise<void> {
|
||||
const event: CreatePatchEvent = {
|
||||
type: EventType.CreatePatch,
|
||||
message,
|
||||
content,
|
||||
creator
|
||||
}
|
||||
await this.sendEvent(event)
|
||||
}
|
||||
|
||||
async createReaction(message: MessageID, reaction: string, creator: SocialID): Promise<void> {
|
||||
const event: CreateReactionEvent = {
|
||||
type: EventType.CreateReaction,
|
||||
message,
|
||||
reaction,
|
||||
creator
|
||||
}
|
||||
await this.sendEvent(event)
|
||||
}
|
||||
|
||||
async removeReaction(message: MessageID, reaction: string, creator: SocialID): Promise<void> {
|
||||
const event: RemoveReactionEvent = {
|
||||
type: EventType.RemoveReaction,
|
||||
message,
|
||||
reaction,
|
||||
creator
|
||||
}
|
||||
await this.sendEvent(event)
|
||||
}
|
||||
|
||||
async createAttachment(message: MessageID, card: CardID, creator: SocialID): Promise<void> {
|
||||
const event: CreateAttachmentEvent = {
|
||||
type: EventType.CreateAttachment,
|
||||
message,
|
||||
card,
|
||||
creator
|
||||
}
|
||||
await this.sendEvent(event)
|
||||
}
|
||||
|
||||
async removeAttachment(message: MessageID, card: CardID): Promise<void> {
|
||||
const event: RemoveAttachmentEvent = {
|
||||
type: EventType.RemoveAttachment,
|
||||
message,
|
||||
card
|
||||
}
|
||||
await this.sendEvent(event)
|
||||
}
|
||||
|
||||
async findMessages(params: FindMessagesParams, queryId?: number): Promise<Message[]> {
|
||||
const rawMessages = await this.ws.send('findMessages', [params, queryId])
|
||||
return rawMessages.map(this.toMessage)
|
||||
}
|
||||
|
||||
toMessage(raw: any): Message {
|
||||
return {
|
||||
id: raw.id,
|
||||
content: raw.content,
|
||||
creator: raw.creator,
|
||||
created: new Date(raw.created),
|
||||
edited: new Date(raw.edited),
|
||||
reactions: raw.reactions,
|
||||
attachments: raw.attachments
|
||||
}
|
||||
}
|
||||
|
||||
async createNotification(message: MessageID, context: ContextID): Promise<void> {
|
||||
const event: CreateNotificationEvent = {
|
||||
type: EventType.CreateNotification,
|
||||
message,
|
||||
context
|
||||
}
|
||||
await this.sendEvent(event)
|
||||
}
|
||||
|
||||
async removeNotification(message: MessageID, context: ContextID): Promise<void> {
|
||||
const event: RemoveNotificationEvent = {
|
||||
type: EventType.RemoveNotification,
|
||||
message,
|
||||
context
|
||||
}
|
||||
await this.sendEvent(event)
|
||||
}
|
||||
|
||||
async createNotificationContext(card: CardID, lastView?: Date, lastUpdate?: Date): Promise<ContextID> {
|
||||
const event: CreateNotificationContextEvent = {
|
||||
type: EventType.CreateNotificationContext,
|
||||
card,
|
||||
lastView,
|
||||
lastUpdate
|
||||
}
|
||||
const result = await this.sendEvent(event)
|
||||
return (result as CreateNotificationContextResult).id
|
||||
}
|
||||
|
||||
async removeNotificationContext(context: ContextID): Promise<void> {
|
||||
const event: RemoveNotificationContextEvent = {
|
||||
type: EventType.RemoveNotificationContext,
|
||||
context
|
||||
}
|
||||
await this.sendEvent(event)
|
||||
}
|
||||
|
||||
async updateNotificationContext(context: ContextID, update: NotificationContextUpdate): Promise<void> {
|
||||
const event: UpdateNotificationContextEvent = {
|
||||
type: EventType.UpdateNotificationContext,
|
||||
context,
|
||||
update
|
||||
}
|
||||
await this.sendEvent(event)
|
||||
}
|
||||
|
||||
async findNotificationContexts(
|
||||
params: FindNotificationContextParams,
|
||||
queryId?: number
|
||||
): Promise<NotificationContext[]> {
|
||||
return await this.ws.send('findNotificationContexts', [params, queryId])
|
||||
}
|
||||
|
||||
async findNotifications(params: FindNotificationsParams, queryId?: number): Promise<Notification[]> {
|
||||
return await this.ws.send('findNotifications', [params, queryId])
|
||||
}
|
||||
|
||||
async unsubscribeQuery(id: number): Promise<void> {
|
||||
await this.ws.send('unsubscribeQuery', [id])
|
||||
}
|
||||
|
||||
private async sendEvent(event: Event): Promise<EventResult> {
|
||||
return await this.ws.send('event', [event])
|
||||
}
|
||||
|
||||
close() {
|
||||
void this.ws.close()
|
||||
}
|
||||
}
|
||||
|
||||
export async function getWebsocketClient(url: string, token: string): Promise<Client> {
|
||||
return new WsClient(url, token)
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import type { Response, HelloRequest, RequestId, BroadcastEvent, Request } from '@communication/sdk-types'
|
||||
import { encode, decode } from '@msgpack/msgpack'
|
||||
|
||||
const PING_TIMEOUT = 10000
|
||||
const RECONNECT_TIMEOUT = 1000
|
||||
|
||||
export class WebSocketConnection {
|
||||
private ws!: WebSocket | Promise<WebSocket>
|
||||
private requests: { [key: RequestId]: { resolve: (response: any) => void; reject: (reason: any) => void } } = {}
|
||||
private lastId: number = 0
|
||||
|
||||
private pingInterval: any
|
||||
private reconnectTimeout: any
|
||||
|
||||
onEvent: (event: BroadcastEvent) => void = () => {}
|
||||
|
||||
constructor(
|
||||
private url: string,
|
||||
private readonly binary: boolean = false
|
||||
) {
|
||||
this.connect()
|
||||
}
|
||||
|
||||
private connect(): void {
|
||||
const ws = new WebSocket(this.url)
|
||||
|
||||
ws.onmessage = (event: MessageEvent) => {
|
||||
const response = deserializeResponse(event.data, this.binary)
|
||||
if (response.id !== undefined) {
|
||||
const handlers = this.requests[response.id]
|
||||
if (handlers === undefined) return
|
||||
delete this.requests[response.id]
|
||||
if (response.error !== undefined) {
|
||||
console.error('Websocket error', response.error)
|
||||
handlers.reject(response.error)
|
||||
} else {
|
||||
handlers.resolve(response.result)
|
||||
}
|
||||
} else {
|
||||
if (response.error !== undefined) {
|
||||
console.error('Websocket error', response.error)
|
||||
} else {
|
||||
const event = response.result as BroadcastEvent
|
||||
this.onEvent(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ws.onclose = () => {
|
||||
clearInterval(this.pingInterval)
|
||||
this.handleReconnect()
|
||||
}
|
||||
|
||||
this.ws = new Promise((resolve, reject) => {
|
||||
ws.onopen = () => {
|
||||
const request: HelloRequest = { id: 'hello', method: 'hello', params: [], binary: this.binary }
|
||||
ws.send(serializeRequest(request, this.binary))
|
||||
clearInterval(this.pingInterval)
|
||||
this.pingInterval = setInterval(() => {
|
||||
void this.sendRequest({ method: 'ping', params: [] })
|
||||
}, PING_TIMEOUT)
|
||||
resolve(ws)
|
||||
}
|
||||
ws.onerror = (event: any) => {
|
||||
console.error('Websocket error', event)
|
||||
reject(new Error('Websocket error'))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private handleReconnect() {
|
||||
clearTimeout(this.reconnectTimeout)
|
||||
this.reconnectTimeout = setTimeout(() => {
|
||||
this.connect()
|
||||
}, RECONNECT_TIMEOUT)
|
||||
}
|
||||
|
||||
async waitWs(): Promise<WebSocket> {
|
||||
if (this.ws instanceof Promise) {
|
||||
this.ws = await this.ws
|
||||
}
|
||||
return this.ws
|
||||
}
|
||||
|
||||
async send(method: string, params: any[]): Promise<any> {
|
||||
const id = ++this.lastId
|
||||
return await this.sendRequest({ id: id.toString(), method, params })
|
||||
}
|
||||
|
||||
private async sendRequest(request: Request): Promise<any> {
|
||||
const ws = await this.waitWs()
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (request.id !== undefined) {
|
||||
this.requests[request.id] = { resolve, reject }
|
||||
}
|
||||
ws.send(serializeRequest(request, this.binary))
|
||||
})
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
clearInterval(this.pingInterval)
|
||||
clearTimeout(this.reconnectTimeout)
|
||||
const ws = await this.waitWs()
|
||||
ws.close()
|
||||
}
|
||||
}
|
||||
|
||||
function serializeRequest(request: Request, binary: boolean): any {
|
||||
if (binary) {
|
||||
return encode(request)
|
||||
} else {
|
||||
return JSON.stringify(request)
|
||||
}
|
||||
}
|
||||
|
||||
function deserializeResponse(data: any, binary: boolean): Response {
|
||||
if (binary) {
|
||||
return decode(data) as Response
|
||||
} else {
|
||||
return JSON.parse(data.toString())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './client'
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import type {
|
||||
CardID,
|
||||
ContextID,
|
||||
FindNotificationContextParams,
|
||||
FindNotificationsParams,
|
||||
Message,
|
||||
MessageID,
|
||||
NotificationContext,
|
||||
NotificationContextUpdate,
|
||||
RichText,
|
||||
SocialID,
|
||||
Notification
|
||||
} from '@communication/types'
|
||||
import type { FindMessagesParams } from '@communication/types'
|
||||
|
||||
import type { BroadcastEvent } from './event.ts'
|
||||
|
||||
export interface Client {
|
||||
createMessage(card: CardID, content: RichText, creator: SocialID): Promise<MessageID>
|
||||
removeMessage(id: MessageID): Promise<void>
|
||||
createPatch(message: MessageID, content: RichText, creator: SocialID): Promise<void>
|
||||
|
||||
createReaction(message: MessageID, reaction: string, creator: SocialID): Promise<void>
|
||||
removeReaction(message: MessageID, reaction: string, creator: SocialID): Promise<void>
|
||||
|
||||
createAttachment(message: MessageID, card: CardID, creator: SocialID): Promise<void>
|
||||
removeAttachment(message: MessageID, card: CardID): Promise<void>
|
||||
|
||||
createNotification(message: MessageID, context: ContextID): Promise<void>
|
||||
removeNotification(message: MessageID, context: ContextID): Promise<void>
|
||||
|
||||
createNotificationContext(card: CardID, lastView?: Date, lastUpdate?: Date): Promise<ContextID>
|
||||
removeNotificationContext(context: ContextID): Promise<void>
|
||||
updateNotificationContext(context: ContextID, update: NotificationContextUpdate): Promise<void>
|
||||
|
||||
onEvent(event: BroadcastEvent): void
|
||||
|
||||
findMessages(params: FindMessagesParams, queryId?: number): Promise<Message[]>
|
||||
findNotificationContexts(params: FindNotificationContextParams, queryId?: number): Promise<NotificationContext[]>
|
||||
findNotifications(params: FindNotificationsParams, queryId?: number): Promise<Notification[]>
|
||||
|
||||
unsubscribeQuery(id: number): Promise<void>
|
||||
close(): void
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './db'
|
||||
export * from './event'
|
||||
export * from './ws'
|
||||
export * from './client'
|
||||
Reference in New Issue
Block a user