@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 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: 10
81
+ maxReconnectAttempts?: number // Maximum reconnect attempts, default: 1000
82
82
  }
83
83
  ```
84
84
 
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- var B;((w)=>{w.DISCONNECTED="disconnected";w.CONNECTING="connecting";w.CONNECTED="connected";w.RECONNECTING="reconnecting"})(B||={});class D{ws=null;eventHandlers=new Map;anyEventHandlers=[];connectionStateHandlers=[];reconnectTimer=null;reconnectAttempts=0;clientId=null;connectionState="disconnected";options={url:"ws://localhost:9696",token:"",reconnect:!0,reconnectInterval:3000,maxReconnectAttempts:10};constructor(k={}){this.options={...this.options,...k}}connect(){return new Promise((k,m)=>{try{if(this.ws)this.ws.close();this.setConnectionState("connecting");let q=this.options.url,A=[];if(this.options.token){A.push("laplace-event-bridge-role-client",this.options.token);let w=new URL(q);w.searchParams.set("token",this.options.token),q=w.toString()}this.ws=new WebSocket(q,A),this.ws.onopen=()=>{this.setConnectionState("connected"),this.reconnectAttempts=0;let w=(()=>{let z=new URL(q);if(z.searchParams.has("token"))z.searchParams.set("token","***");return z.toString()})();console.log(`Connected to LAPLACE Event Bridge: ${w}`),k()},this.ws.onmessage=(w)=>{try{let z=JSON.parse(w.data);if(z.type==="ping"){this.ws?.send(JSON.stringify({type:"pong",timestamp:Date.now(),respondingTo:z.timestamp}));return}if(z.type==="established"&&z.clientId)this.clientId=z.clientId,console.log(`Connection established with client ID: ${this.clientId}`);this.processEvent(z)}catch(z){console.error("Failed to parse event data:",z)}},this.ws.onerror=(w)=>{console.error("WebSocket error:",w),m(w)},this.ws.onclose=()=>{if(console.log("Disconnected from LAPLACE Event Bridge"),this.options.reconnect&&this.reconnectAttempts<this.options.maxReconnectAttempts)this.reconnectAttempts++,this.setConnectionState("reconnecting"),console.log(`Attempting to reconnect (${this.reconnectAttempts}/${this.options.maxReconnectAttempts})...`),this.reconnectTimer=setTimeout(()=>{this.connect().catch((w)=>{console.error("Reconnection failed:",w)})},this.options.reconnectInterval);else this.setConnectionState("disconnected")}}catch(q){this.setConnectionState("disconnected"),m(q)}})}disconnect(){if(this.reconnectTimer)clearTimeout(this.reconnectTimer),this.reconnectTimer=null;if(this.ws)this.ws.close(),this.ws=null;this.setConnectionState("disconnected"),this.clientId=null}on(k,m){if(!this.eventHandlers.has(k))this.eventHandlers.set(k,[]);this.eventHandlers.get(k).push(m)}onAny(k){this.anyEventHandlers.push(k)}onConnectionStateChange(k){this.connectionStateHandlers.push(k),k(this.connectionState)}off(k,m){if(!this.eventHandlers.has(k))return;let q=this.eventHandlers.get(k),A=q.indexOf(m);if(A!==-1)q.splice(A,1);if(q.length===0)this.eventHandlers.delete(k)}offAny(k){let m=this.anyEventHandlers.indexOf(k);if(m!==-1)this.anyEventHandlers.splice(m,1)}offConnectionStateChange(k){let m=this.connectionStateHandlers.indexOf(k);if(m!==-1)this.connectionStateHandlers.splice(m,1)}isConnectedToBridge(){return this.connectionState==="connected"}getConnectionState(){return this.connectionState}getClientId(){return this.clientId}send(k){if(!this.ws||this.ws.readyState!==WebSocket.OPEN)throw new Error("Not connected to LAPLACE Event Bridge");this.ws.send(JSON.stringify(k))}setConnectionState(k){if(this.connectionState!==k){this.connectionState=k;for(let m of this.connectionStateHandlers)try{m(k)}catch(q){console.error("Error in connection state change handler:",q)}}}processEvent(k){if(this.eventHandlers.has(k.type))for(let m of this.eventHandlers.get(k.type))try{m(k)}catch(q){console.error(`Error in event handler for type ${k.type}:`,q)}for(let m of this.anyEventHandlers)try{m(k)}catch(q){console.error("Error in any event handler:",q)}}}export{D as LaplaceEventBridgeClient,B as ConnectionState};
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 10
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: number | null = null
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: 10,
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' && data.clientId) {
147
+ if (data.type === 'established') {
135
148
  this.clientId = data.clientId
136
- console.log(`Connection established with client ID: ${this.clientId}`)
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
- console.log(`Attempting to reconnect (${this.reconnectAttempts}/${this.options.maxReconnectAttempts})...`)
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
- }, this.options.reconnectInterval) as unknown as number
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.1",
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
  }