1+ //
2+ // Copyright © 2024-2025 Hardcore Engineering Inc.
3+ //
4+ // Licensed under the Eclipse Public License, Version 2.0 (the "License");
5+ // you may not use this file except in compliance with the License. You may
6+ // obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
7+ //
8+ // Unless required by applicable law or agreed to in writing, software
9+ // distributed under the License is distributed on an "AS IS" BASIS,
10+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+ //
12+ // See the License for the specific language governing permissions and
13+ // limitations under the License.
14+ //
15+
16+ // import { WebSocket } from 'ws'; // для Node <20 обязательно
17+
18+ // Unknown:
19+ // import { type Ref, concatLink } from '@hcengineering/core'
20+ // import { getMetadata } from '@hcengineering/platform'
21+
22+ // import { getCurrentEmployee, type Person } from '@hcengineering/contact'
23+ // import presence from '@hcengineering/presence'
24+ // import presentation from '@hcengineering/presentation'
25+ // import { type Unsubscriber, get } from 'svelte/store'
26+
27+ // import { myPresence, myData, isAnybodyInMyRoom, onPersonUpdate, onPersonLeave, onPersonData } from './store'
28+ // import type { RoomPresence, MyDataItem } from './types'
29+
30+ // interface PresenceMessage {
31+ // id: Ref<Person>
32+ // type: 'update' | 'remove'
33+ // presence?: RoomPresence[]
34+ // lastUpdate?: number
35+ // }
36+
37+ // interface DataMessage {
38+ // type: 'data'
39+ // sender: Ref<Person>
40+ // topic: string
41+ // data: any
42+ // }
43+
44+ // type IncomingMessage = PresenceMessage | DataMessage
45+
46+
47+
48+ export class HulypulseClient implements Disposable {
49+ private ws : WebSocket | null = null
50+ private closed = false
51+ private reconnectTimeout : number | undefined
52+ private pingTimeout : number | undefined
53+ private pingInterval : number | undefined
54+ private readonly RECONNECT_INTERVAL = 1000
55+ private readonly PING_INTERVAL = 30 * 1000
56+ private readonly PING_TIMEOUT = 5 * 60 * 1000
57+ private readonly myDataThrottleInterval = 100
58+ // private readonly url: string | URL = 'ws://localhost:8095'
59+
60+ // private presence: RoomPresence[]
61+ private readonly myDataTimestamps = new Map < string , number > ( )
62+ // // private readonly myPresenceUnsub: Unsubscriber
63+ // private readonly myDataUnsub: Unsubscriber
64+
65+ constructor ( private readonly url : string | URL ) {
66+ // this.presence = get(myPresence)
67+ // this.myPresenceUnsub = myPresence.subscribe((presence) => {
68+ // this.handlePresenceChanged(presence)
69+ // })
70+ // this.myDataUnsub = myData.subscribe((data) => {
71+ // this.handleMyDataChanged(data, false)
72+ // })
73+
74+ this . connect ( )
75+ }
76+
77+ // Close the connection
78+ close ( ) : void {
79+ console . log ( 'Closing connection' )
80+ this . closed = true
81+ clearTimeout ( this . reconnectTimeout )
82+ this . stopPing ( )
83+
84+ // this.myPresenceUnsub()
85+ // this.myDataUnsub()
86+
87+ if ( this . ws !== null ) {
88+ this . ws . close ( )
89+ this . ws = null
90+ }
91+ }
92+
93+ // Open the connection and reconnect if it fails
94+ private connect ( ) : void {
95+ console . log ( 'Connecting to WebSocket: ' , this . url )
96+ try {
97+ const ws = new WebSocket ( this . url )
98+ console . log ( 'WebSocket created: ' , ws )
99+ this . ws = ws
100+
101+ ws . onopen = ( ) => {
102+ console . log ( 'WebSocket.onopen' )
103+ if ( this . ws !== ws ) {
104+ return
105+ }
106+
107+ this . handleConnect ( )
108+ }
109+
110+ ws . onclose = ( event : CloseEvent ) => {
111+ console . log ( 'WebSocket.onclose' )
112+ if ( this . ws !== ws ) {
113+ ws . close ( )
114+ return
115+ }
116+
117+ this . reconnect ( )
118+ }
119+
120+ ws . onmessage = ( event : MessageEvent ) => {
121+ console . log ( 'WebSocket.onmessage: ' , event . data )
122+ if ( this . closed || this . ws !== ws ) {
123+ return
124+ }
125+
126+ this . handleMessage ( event . data )
127+ }
128+
129+ ws . onerror = ( event : Event ) => {
130+ console . log ( 'client websocket error' , event )
131+ if ( this . ws !== ws ) {
132+ return
133+ }
134+ }
135+ } catch ( err : any ) {
136+ console . error ( 'WebSocket error' , err )
137+ this . reconnect ( )
138+ }
139+ }
140+
141+ private startPing ( ) : void {
142+ console . log ( 'Starting ping' )
143+ clearInterval ( this . pingInterval )
144+ this . pingInterval = window . setInterval ( ( ) => {
145+ if ( this . ws !== null && this . ws . readyState === WebSocket . OPEN ) {
146+ this . ws . send ( 'ping' )
147+ }
148+ clearTimeout ( this . pingTimeout )
149+ this . pingTimeout = window . setTimeout ( ( ) => {
150+ if ( this . ws !== null ) {
151+ console . log ( 'no response from server' )
152+ clearInterval ( this . pingInterval )
153+ this . ws . close ( 1000 )
154+ }
155+ } , this . PING_TIMEOUT )
156+ } , this . PING_INTERVAL )
157+ }
158+
159+ private stopPing ( ) : void {
160+ console . log ( 'Stopping ping' )
161+ clearInterval ( this . pingInterval )
162+ this . pingInterval = undefined
163+
164+ clearTimeout ( this . pingTimeout )
165+ this . pingTimeout = undefined
166+ }
167+
168+ private reconnect ( ) : void {
169+ console . log ( 'Reconnecting...' )
170+ clearTimeout ( this . reconnectTimeout )
171+ this . stopPing ( )
172+
173+ if ( ! this . closed ) {
174+ this . reconnectTimeout = window . setTimeout ( ( ) => {
175+ this . connect ( )
176+ } , this . RECONNECT_INTERVAL )
177+ }
178+ }
179+
180+ private handleConnect ( ) : void {
181+ // this.sendPresence(getCurrentEmployee(), this.presence)
182+ this . startPing ( )
183+ // this.handleMyDataChanged(get(myData), true)
184+ }
185+
186+ private handleMessage ( data : string ) : void {
187+ if ( data === 'pong' ) {
188+ clearTimeout ( this . pingTimeout )
189+ return
190+ }
191+
192+ try {
193+ const message = JSON . parse ( data ) ; // as IncomingMessage
194+ console . log ( 'Received message' , message ) ;
195+ // const message = JSON.parse(data) as IncomingMessage
196+ // if (message.type === 'update' && message.presence !== undefined) {
197+ // onPersonUpdate(message.id, message.presence ?? [])
198+ // } else if (message.type === 'remove') {
199+ // onPersonLeave(message.id)
200+ // } else if (message.type === 'data') {
201+ // onPersonData(message.sender, message.topic, message.data)
202+ // } else {
203+ // console.warn('Unknown message type', message)
204+ // }
205+ } catch ( err : any ) {
206+ console . error ( 'Error parsing message' , err , data )
207+ }
208+ }
209+
210+ // private handlePresenceChanged (presence: RoomPresence[]): void {
211+ // this.presence = presence
212+ // this.sendPresence(getCurrentEmployee(), this.presence)
213+ // this.handleMyDataChanged(get(myData), true)
214+ // }
215+
216+ // private sendPresence (person: Ref<Person>, presence: RoomPresence[]): void {
217+ // if (!this.closed && this.ws !== null && this.ws.readyState === WebSocket.OPEN) {
218+ // const message: PresenceMessage = { id: person, type: 'update', presence }
219+ // this.ws.send(JSON.stringify(message))
220+ // }
221+ // }
222+
223+ // private handleMyDataChanged (data: Map<string, MyDataItem>, forceSend: boolean): void {
224+ // if (!isAnybodyInMyRoom()) {
225+ // return
226+ // }
227+ // if (!this.closed && this.ws !== null && this.ws.readyState === WebSocket.OPEN) {
228+ // for (const [topic, value] of data) {
229+ // const lastSend = this.myDataTimestamps.get(topic) ?? 0
230+ // if (value.lastUpdated >= lastSend + this.myDataThrottleInterval || forceSend) {
231+ // this.myDataTimestamps.set(topic, value.lastUpdated)
232+ // const message: DataMessage = {
233+ // sender: getCurrentEmployee(),
234+ // type: 'data',
235+ // topic,
236+ // data: value.data
237+ // }
238+ // this.ws.send(JSON.stringify(message))
239+ // }
240+ // }
241+ // }
242+ // }
243+
244+ [ Symbol . dispose ] ( ) : void {
245+ this . close ( )
246+ }
247+ }
248+
249+ export function connect ( ) : HulypulseClient | undefined {
250+ // const wsUuid = getMetadata(presentation.metadata.WorkspaceUuid)
251+ // if (wsUuid === undefined) {
252+ // console.warn('Workspace uuid is not defined')
253+ // return undefined
254+ // }
255+
256+ // const token = getMetadata(presentation.metadata.Token)
257+
258+ // const presenceUrl = getMetadata(presence.metadata.PresenceUrl)
259+ // if (presenceUrl === undefined || presenceUrl === '') {
260+ // console.warn('Presence URL is not defined')
261+ // return undefined
262+ // }
263+
264+ // const url = new URL(concatLink(presenceUrl, wsUuid))
265+ // if (token !== undefined) {
266+ // url.searchParams.set('token', token)
267+ // }
268+
269+ // return new HulypulseClient(url)
270+ return new HulypulseClient ( "ws://localhost:8095" )
271+ }
0 commit comments