@laplace.live/event-bridge-sdk 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.js +1 -1
- package/index.ts +150 -19
- package/package.json +1 -4
package/README.md
CHANGED
|
@@ -78,7 +78,7 @@ interface ConnectionOptions {
|
|
|
78
78
|
token?: string // Authentication token, default: ''
|
|
79
79
|
reconnect?: boolean // Auto reconnect on disconnect, default: true
|
|
80
80
|
reconnectInterval?: number // Milliseconds between reconnect attempts, default: 3000
|
|
81
|
-
maxReconnectAttempts?: number // Maximum reconnect attempts, default:
|
|
81
|
+
maxReconnectAttempts?: number // Maximum reconnect attempts, default: 1000
|
|
82
82
|
}
|
|
83
83
|
```
|
|
84
84
|
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var K;((A)=>{A.DISCONNECTED="disconnected";A.CONNECTING="connecting";A.CONNECTED="connected";A.RECONNECTING="reconnecting"})(K||={});class N{ws=null;eventHandlers=new Map;anyEventHandlers=[];connectionStateHandlers=[];reconnectTimer=null;reconnectAttempts=0;clientId=null;serverVersion=null;connectionState="disconnected";lastPingTime=null;pingMonitorTimer=null;options={url:"ws://localhost:9696",token:"",reconnect:!0,reconnectInterval:3000,maxReconnectAttempts:1000,pingTimeout:90000};constructor(q={}){this.options={...this.options,...q}}connect(){return new Promise((q,w)=>{try{if(this.ws)this.ws.close();this.setConnectionState("connecting");let z=this.options.url,E=[];if(this.options.token){E.push("laplace-event-bridge-role-client",this.options.token);let A=new URL(z);A.searchParams.set("token",this.options.token),z=A.toString()}this.ws=new WebSocket(z,E),this.ws.onopen=()=>{this.setConnectionState("connected"),this.reconnectAttempts=0,q()},this.ws.onmessage=(A)=>{try{let B=JSON.parse(A.data);if(B.type==="ping"){this.lastPingTime=Date.now(),this.ws?.send(JSON.stringify({type:"pong",timestamp:Date.now(),respondingTo:B.timestamp}));return}if(B.type==="established"){this.clientId=B.clientId,this.serverVersion=B.version;let G=(()=>{let F=new URL(z);if(F.searchParams.has("token"))F.searchParams.set("token","***");return F.toString()})();if(console.log(`Welcome to LAPLACE Event Bridge ${`v${B.version}`||"(unknown version)"}: ${G} with client ID ${this.clientId||"unknown"}`),this.shouldMonitorPing())this.startPingMonitoring()}this.processEvent(B)}catch(B){console.error("Failed to parse event data:",B)}},this.ws.onerror=(A)=>{console.error("WebSocket error:",A),w(A)},this.ws.onclose=()=>{if(console.log("Disconnected from LAPLACE Event Bridge"),this.stopPingMonitoring(),this.lastPingTime=null,this.options.reconnect&&this.reconnectAttempts<this.options.maxReconnectAttempts){this.reconnectAttempts++,this.setConnectionState("reconnecting");let A=this.options.reconnectInterval,B=1.5,G=60000,F=Math.min(A*Math.pow(B,this.reconnectAttempts-1),G),H=Math.round(F);console.log(`Attempting to reconnect (${this.reconnectAttempts}/${this.options.maxReconnectAttempts}) in ${H}ms...`),this.reconnectTimer=setTimeout(()=>{this.connect().catch((J)=>{console.error("Reconnection failed:",J)})},H)}else this.setConnectionState("disconnected")}}catch(z){this.setConnectionState("disconnected"),w(z)}})}disconnect(){if(this.reconnectTimer)clearTimeout(this.reconnectTimer),this.reconnectTimer=null;if(this.stopPingMonitoring(),this.ws)this.ws.close(),this.ws=null;this.setConnectionState("disconnected"),this.clientId=null,this.serverVersion=null,this.lastPingTime=null}on(q,w){if(!this.eventHandlers.has(q))this.eventHandlers.set(q,[]);this.eventHandlers.get(q).push(w)}onAny(q){this.anyEventHandlers.push(q)}onConnectionStateChange(q){this.connectionStateHandlers.push(q),q(this.connectionState)}off(q,w){if(!this.eventHandlers.has(q))return;let z=this.eventHandlers.get(q),E=z.indexOf(w);if(E!==-1)z.splice(E,1);if(z.length===0)this.eventHandlers.delete(q)}offAny(q){let w=this.anyEventHandlers.indexOf(q);if(w!==-1)this.anyEventHandlers.splice(w,1)}offConnectionStateChange(q){let w=this.connectionStateHandlers.indexOf(q);if(w!==-1)this.connectionStateHandlers.splice(w,1)}isConnectedToBridge(){return this.connectionState==="connected"}getConnectionState(){return this.connectionState}getClientId(){return this.clientId}send(q){if(!this.ws||this.ws.readyState!==WebSocket.OPEN)throw new Error("Not connected to LAPLACE Event Bridge");this.ws.send(JSON.stringify(q))}setConnectionState(q){if(this.connectionState!==q){this.connectionState=q;for(let w of this.connectionStateHandlers)try{w(q)}catch(z){console.error("Error in connection state change handler:",z)}}}processEvent(q){if(this.eventHandlers.has(q.type))for(let w of this.eventHandlers.get(q.type))try{w(q)}catch(z){console.error(`Error in event handler for type ${q.type}:`,z)}for(let w of this.anyEventHandlers)try{w(q)}catch(z){console.error("Error in any event handler:",z)}}shouldMonitorPing(){if(!this.serverVersion)return!1;let q=this.serverVersion.split(".").map((A)=>parseInt(A,10));if(q.length<3||q.some(isNaN))return console.warn(`Invalid server version format: ${this.serverVersion}`),!1;let w=q[0],z=q[1],E=q[2];if(w>4)return!0;if(w===4){if(z>0)return!0;if(z===0&&E>=3)return!0}return!1}startPingMonitoring(){this.stopPingMonitoring(),console.log(`Ping monitoring enabled (timeout: ${this.options.pingTimeout}ms)`),this.lastPingTime=Date.now(),this.pingMonitorTimer=setInterval(()=>{if(!this.lastPingTime)return;let q=Date.now()-this.lastPingTime;if(q>this.options.pingTimeout){if(console.warn(`Ping timeout detected (${q}ms since last ping). Reconnecting...`),this.stopPingMonitoring(),this.ws)this.ws.close()}},this.options.pingTimeout/3)}stopPingMonitoring(){if(this.pingMonitorTimer)clearInterval(this.pingMonitorTimer),this.pingMonitorTimer=null}}export{N as LaplaceEventBridgeClient,K as ConnectionState};
|
package/index.ts
CHANGED
|
@@ -34,17 +34,34 @@ export interface ConnectionOptions {
|
|
|
34
34
|
*/
|
|
35
35
|
reconnect?: boolean
|
|
36
36
|
/**
|
|
37
|
-
* The interval between reconnect attempts in milliseconds
|
|
37
|
+
* The base interval between reconnect attempts in milliseconds.
|
|
38
|
+
* With exponential backoff, each attempt multiplies this by 1.5^(attempt-1).
|
|
39
|
+
* The maximum interval is capped at 60 seconds.
|
|
38
40
|
*
|
|
39
41
|
* @default 3000
|
|
42
|
+
* @example
|
|
43
|
+
* // With base interval of 3000ms:
|
|
44
|
+
* // Attempt 1: 3000ms
|
|
45
|
+
* // Attempt 2: 4500ms
|
|
46
|
+
* // Attempt 3: 6750ms
|
|
47
|
+
* // ...
|
|
48
|
+
* // Capped at: 60000ms
|
|
40
49
|
*/
|
|
41
50
|
reconnectInterval?: number
|
|
42
51
|
/**
|
|
43
52
|
* The maximum number of reconnect attempts
|
|
44
53
|
*
|
|
45
|
-
* @default
|
|
54
|
+
* @default 1000
|
|
46
55
|
*/
|
|
47
56
|
maxReconnectAttempts?: number
|
|
57
|
+
/**
|
|
58
|
+
* The timeout for ping heartbeat in milliseconds
|
|
59
|
+
* If no ping is received within this time, the connection is considered dead
|
|
60
|
+
* Only applies to server versions >= 4.0.2
|
|
61
|
+
*
|
|
62
|
+
* @default 90000 (90 seconds)
|
|
63
|
+
*/
|
|
64
|
+
pingTimeout?: number
|
|
48
65
|
}
|
|
49
66
|
|
|
50
67
|
export class LaplaceEventBridgeClient {
|
|
@@ -52,17 +69,21 @@ export class LaplaceEventBridgeClient {
|
|
|
52
69
|
private eventHandlers = new Map<string, EventHandler<any>[]>()
|
|
53
70
|
private anyEventHandlers: AnyEventHandler[] = []
|
|
54
71
|
private connectionStateHandlers: ConnectionStateChangeHandler[] = []
|
|
55
|
-
private reconnectTimer:
|
|
72
|
+
private reconnectTimer: ReturnType<typeof setTimeout> | null = null
|
|
56
73
|
private reconnectAttempts = 0
|
|
57
74
|
private clientId: string | null = null
|
|
75
|
+
private serverVersion: string | null = null
|
|
58
76
|
private connectionState: ConnectionState = ConnectionState.DISCONNECTED
|
|
77
|
+
private lastPingTime: number | null = null
|
|
78
|
+
private pingMonitorTimer: ReturnType<typeof setInterval> | null = null
|
|
59
79
|
|
|
60
80
|
private options: Required<ConnectionOptions> = {
|
|
61
81
|
url: 'ws://localhost:9696',
|
|
62
82
|
token: '',
|
|
63
83
|
reconnect: true,
|
|
64
84
|
reconnectInterval: 3000,
|
|
65
|
-
maxReconnectAttempts:
|
|
85
|
+
maxReconnectAttempts: 1000,
|
|
86
|
+
pingTimeout: 90000, // 90 seconds
|
|
66
87
|
}
|
|
67
88
|
|
|
68
89
|
constructor(options: ConnectionOptions = {}) {
|
|
@@ -99,17 +120,6 @@ export class LaplaceEventBridgeClient {
|
|
|
99
120
|
this.ws.onopen = () => {
|
|
100
121
|
this.setConnectionState(ConnectionState.CONNECTED)
|
|
101
122
|
this.reconnectAttempts = 0
|
|
102
|
-
|
|
103
|
-
// Create a display URL that masks the token if present
|
|
104
|
-
const displayUrl = (() => {
|
|
105
|
-
const urlObj = new URL(url)
|
|
106
|
-
if (urlObj.searchParams.has('token')) {
|
|
107
|
-
urlObj.searchParams.set('token', '***')
|
|
108
|
-
}
|
|
109
|
-
return urlObj.toString()
|
|
110
|
-
})()
|
|
111
|
-
|
|
112
|
-
console.log(`Connected to LAPLACE Event Bridge: ${displayUrl}`)
|
|
113
123
|
resolve()
|
|
114
124
|
}
|
|
115
125
|
|
|
@@ -119,6 +129,9 @@ export class LaplaceEventBridgeClient {
|
|
|
119
129
|
|
|
120
130
|
// Handle ping from server
|
|
121
131
|
if (data.type === 'ping') {
|
|
132
|
+
// Track last ping time
|
|
133
|
+
this.lastPingTime = Date.now()
|
|
134
|
+
|
|
122
135
|
// Respond with pong
|
|
123
136
|
this.ws?.send(
|
|
124
137
|
JSON.stringify({
|
|
@@ -131,9 +144,27 @@ export class LaplaceEventBridgeClient {
|
|
|
131
144
|
}
|
|
132
145
|
|
|
133
146
|
// Store client ID from the established message
|
|
134
|
-
if (data.type === 'established'
|
|
147
|
+
if (data.type === 'established') {
|
|
135
148
|
this.clientId = data.clientId
|
|
136
|
-
|
|
149
|
+
this.serverVersion = data.version
|
|
150
|
+
|
|
151
|
+
// Create a display URL that masks the token if present
|
|
152
|
+
const displayUrl = (() => {
|
|
153
|
+
const urlObj = new URL(url)
|
|
154
|
+
if (urlObj.searchParams.has('token')) {
|
|
155
|
+
urlObj.searchParams.set('token', '***')
|
|
156
|
+
}
|
|
157
|
+
return urlObj.toString()
|
|
158
|
+
})()
|
|
159
|
+
|
|
160
|
+
console.log(
|
|
161
|
+
`Welcome to LAPLACE Event Bridge ${`v${data.version}` || '(unknown version)'}: ${displayUrl} with client ID ${this.clientId || 'unknown'}`
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
// Start ping monitoring if server version supports it (>= 4.0.2)
|
|
165
|
+
if (this.shouldMonitorPing()) {
|
|
166
|
+
this.startPingMonitoring()
|
|
167
|
+
}
|
|
137
168
|
}
|
|
138
169
|
|
|
139
170
|
// Process the event
|
|
@@ -151,15 +182,36 @@ export class LaplaceEventBridgeClient {
|
|
|
151
182
|
this.ws.onclose = () => {
|
|
152
183
|
console.log('Disconnected from LAPLACE Event Bridge')
|
|
153
184
|
|
|
185
|
+
// Stop ping monitoring before attempting reconnection
|
|
186
|
+
this.stopPingMonitoring()
|
|
187
|
+
|
|
188
|
+
// Clear ping state
|
|
189
|
+
this.lastPingTime = null
|
|
190
|
+
|
|
154
191
|
if (this.options.reconnect && this.reconnectAttempts < this.options.maxReconnectAttempts) {
|
|
155
192
|
this.reconnectAttempts++
|
|
156
193
|
this.setConnectionState(ConnectionState.RECONNECTING)
|
|
157
|
-
|
|
194
|
+
|
|
195
|
+
// Calculate exponential backoff with cap at 60 seconds
|
|
196
|
+
const baseInterval = this.options.reconnectInterval
|
|
197
|
+
const backoffMultiplier = 1.5 // Increase by 50% each time
|
|
198
|
+
const maxInterval = 60000 // 60 seconds cap
|
|
199
|
+
|
|
200
|
+
// Calculate delay: base * (multiplier ^ (attempt - 1))
|
|
201
|
+
const calculatedDelay = Math.min(
|
|
202
|
+
baseInterval * Math.pow(backoffMultiplier, this.reconnectAttempts - 1),
|
|
203
|
+
maxInterval
|
|
204
|
+
)
|
|
205
|
+
const delay = Math.round(calculatedDelay)
|
|
206
|
+
|
|
207
|
+
console.log(
|
|
208
|
+
`Attempting to reconnect (${this.reconnectAttempts}/${this.options.maxReconnectAttempts}) in ${delay}ms...`
|
|
209
|
+
)
|
|
158
210
|
this.reconnectTimer = setTimeout(() => {
|
|
159
211
|
this.connect().catch(err => {
|
|
160
212
|
console.error('Reconnection failed:', err)
|
|
161
213
|
})
|
|
162
|
-
},
|
|
214
|
+
}, delay)
|
|
163
215
|
} else {
|
|
164
216
|
this.setConnectionState(ConnectionState.DISCONNECTED)
|
|
165
217
|
}
|
|
@@ -180,6 +232,8 @@ export class LaplaceEventBridgeClient {
|
|
|
180
232
|
this.reconnectTimer = null
|
|
181
233
|
}
|
|
182
234
|
|
|
235
|
+
this.stopPingMonitoring()
|
|
236
|
+
|
|
183
237
|
if (this.ws) {
|
|
184
238
|
this.ws.close()
|
|
185
239
|
this.ws = null
|
|
@@ -187,6 +241,8 @@ export class LaplaceEventBridgeClient {
|
|
|
187
241
|
|
|
188
242
|
this.setConnectionState(ConnectionState.DISCONNECTED)
|
|
189
243
|
this.clientId = null
|
|
244
|
+
this.serverVersion = null
|
|
245
|
+
this.lastPingTime = null
|
|
190
246
|
}
|
|
191
247
|
|
|
192
248
|
/**
|
|
@@ -332,4 +388,79 @@ export class LaplaceEventBridgeClient {
|
|
|
332
388
|
}
|
|
333
389
|
}
|
|
334
390
|
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Check if ping monitoring should be enabled based on server version
|
|
394
|
+
*/
|
|
395
|
+
private shouldMonitorPing(): boolean {
|
|
396
|
+
if (!this.serverVersion) {
|
|
397
|
+
return false
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Parse version (e.g., "4.0.2" -> [4, 0, 2])
|
|
401
|
+
const versionParts = this.serverVersion.split('.').map(part => parseInt(part, 10))
|
|
402
|
+
|
|
403
|
+
// Ensure we have at least 3 version parts
|
|
404
|
+
if (versionParts.length < 3 || versionParts.some(isNaN)) {
|
|
405
|
+
console.warn(`Invalid server version format: ${this.serverVersion}`)
|
|
406
|
+
return false
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const major = versionParts[0]!
|
|
410
|
+
const minor = versionParts[1]!
|
|
411
|
+
const patch = versionParts[2]!
|
|
412
|
+
|
|
413
|
+
// Check if version is >= 4.0.3
|
|
414
|
+
if (major > 4) return true
|
|
415
|
+
if (major === 4) {
|
|
416
|
+
if (minor > 0) return true
|
|
417
|
+
if (minor === 0 && patch >= 3) return true
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return false
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Start monitoring ping messages from the server
|
|
425
|
+
*/
|
|
426
|
+
private startPingMonitoring(): void {
|
|
427
|
+
// Stop any existing monitoring
|
|
428
|
+
this.stopPingMonitoring()
|
|
429
|
+
|
|
430
|
+
console.log(`Ping monitoring enabled (timeout: ${this.options.pingTimeout}ms)`)
|
|
431
|
+
|
|
432
|
+
// Set initial ping time
|
|
433
|
+
this.lastPingTime = Date.now()
|
|
434
|
+
|
|
435
|
+
// Start monitoring
|
|
436
|
+
this.pingMonitorTimer = setInterval(() => {
|
|
437
|
+
if (!this.lastPingTime) {
|
|
438
|
+
return
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const timeSinceLastPing = Date.now() - this.lastPingTime
|
|
442
|
+
|
|
443
|
+
if (timeSinceLastPing > this.options.pingTimeout) {
|
|
444
|
+
console.warn(`Ping timeout detected (${timeSinceLastPing}ms since last ping). Reconnecting...`)
|
|
445
|
+
|
|
446
|
+
// Stop monitoring
|
|
447
|
+
this.stopPingMonitoring()
|
|
448
|
+
|
|
449
|
+
// Force reconnection
|
|
450
|
+
if (this.ws) {
|
|
451
|
+
this.ws.close()
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}, this.options.pingTimeout / 3) // Check 3 times within the timeout period
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Stop monitoring ping messages
|
|
459
|
+
*/
|
|
460
|
+
private stopPingMonitoring(): void {
|
|
461
|
+
if (this.pingMonitorTimer) {
|
|
462
|
+
clearInterval(this.pingMonitorTimer)
|
|
463
|
+
this.pingMonitorTimer = null
|
|
464
|
+
}
|
|
465
|
+
}
|
|
335
466
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@laplace.live/event-bridge-sdk",
|
|
3
3
|
"description": "LAPLACE Event Bridge SDK",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.3",
|
|
5
5
|
"module": "index.ts",
|
|
6
6
|
"types": "index.ts",
|
|
7
7
|
"license": "MIT",
|
|
@@ -24,9 +24,6 @@
|
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@laplace.live/event-types": "^2.0.12"
|
|
26
26
|
},
|
|
27
|
-
"peerDependencies": {
|
|
28
|
-
"@laplace.live/event-types": "^2.0.4"
|
|
29
|
-
},
|
|
30
27
|
"devDependencies": {
|
|
31
28
|
"bun-types": "latest"
|
|
32
29
|
}
|