Init ws client (#7)

* Init client-ws
This commit is contained in:
Kristina
2024-12-25 17:02:59 +04:00
committed by GitHub
parent 2799e8bb54
commit 7bf170cd03
8 changed files with 410 additions and 0 deletions
BIN
View File
Binary file not shown.
+18
View File
@@ -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"
}
}
+213
View File
@@ -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)
}
+123
View File
@@ -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())
}
}
+1
View File
@@ -0,0 +1 @@
export * from './client'
+9
View File
@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"jsx": "react-jsx",
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src"]
}
+45
View File
@@ -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
View File
@@ -1,3 +1,4 @@
export * from './db'
export * from './event'
export * from './ws'
export * from './client'