Skip to content

Commit 4bd387b

Browse files
committed
ping-pong heartbeat added
Signed-off-by: Leonid Kaganov <lleo@lleo.me>
1 parent 3821b8b commit 4bd387b

File tree

9 files changed

+373
-17
lines changed

9 files changed

+373
-17
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "hulypulse"
3-
version = "0.1.16"
3+
version = "0.1.20"
44
edition = "2024"
55

66
[dependencies]

client/off/client.ts

Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
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+
}

scripts/TEST_no_auth.html

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ <h1>WebSocket JSON Tester</h1>
6767

6868
<p>
6969

70+
<button onclick="place()" data='ping'>ping</button>
71+
7072
<button onclick="place()" data='{"type":"put", "key":"00000000-0000-0000-0000-000000000001/foo/bar1", "data": "hello 1", "TTL":5, "correlation": "abc123"}'>PUT 1</button>
7173
<button onclick="place()" data='{"type":"put", "key":"00000000-0000-0000-0000-000000000001/foo/bar2", "data": "hello 2", "TTL":9, "correlation": "abc123"}'>PUT 2</button>
7274

@@ -124,6 +126,12 @@ <h1>WebSocket JSON Tester</h1>
124126
const json = ( textarea.value || textarea.getAttribute('placeholder') ).trim();
125127

126128
try {
129+
if(json=="ping") {
130+
ws.send(json);
131+
pr("📤 Sent: ping");
132+
return;
133+
}
134+
127135
const parsed = JSON.parse(json); // validate JSON
128136
ws.send(JSON.stringify(parsed));
129137
pr("📤 Sent:\n" + JSON.stringify(parsed, null, 2));
@@ -138,4 +146,4 @@ <h1>WebSocket JSON Tester</h1>
138146
</script>
139147

140148
</body>
141-
</html>
149+
</html>

src/config.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ pub struct Config {
5656

5757
pub backend: BackendType,
5858
pub no_authorization: bool,
59+
60+
pub heartbeat_timeout: u64,
5961
}
6062

6163
pub static CONFIG: LazyLock<Config> = LazyLock::new(|| {

src/config/default.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,7 @@ max_ttl = 3600
1212
backend = "redis"
1313
no_authorization = false
1414

15+
heartbeat_timeout = 30
16+
1517
# optional settings
1618
# max_size = 100

0 commit comments

Comments
 (0)