@shipload/sdk 1.0.0-next.1 → 1.0.0-next.2

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shipload/sdk",
3
3
  "description": "SDKs for Shipload",
4
- "version": "1.0.0-next.1",
4
+ "version": "1.0.0-next.2",
5
5
  "homepage": "https://github.com/shipload/toolkit/tree/master/packages/sdk",
6
6
  "repository": {
7
7
  "type": "git",
@@ -7,6 +7,9 @@ export interface WebSocketConnectionOptions {
7
7
  url: string
8
8
  onMessage: (message: ServerMessage) => void
9
9
  onStateChange?: (state: ConnectionState) => void
10
+ minReconnectDelay?: number
11
+ pingIntervalMs?: number
12
+ pongTimeoutMs?: number
10
13
  }
11
14
 
12
15
  export class WebSocketConnection {
@@ -19,15 +22,26 @@ export class WebSocketConnection {
19
22
  private _state: ConnectionState = 'disconnected'
20
23
  private shouldReconnect = true
21
24
  private sendQueue: string[] = []
25
+ private minReconnectDelay: number
26
+ private pingIntervalMs: number
27
+ private pongTimeoutMs: number
28
+ private pingTimer: ReturnType<typeof setInterval> | null = null
29
+ private staleTimer: ReturnType<typeof setTimeout> | null = null
22
30
 
23
- private static readonly MIN_RECONNECT_DELAY = 1000
31
+ private static readonly DEFAULT_MIN_RECONNECT_DELAY = 1000
24
32
  private static readonly MAX_RECONNECT_DELAY = 30000
25
33
  private static readonly RECONNECT_MULTIPLIER = 2
34
+ private static readonly DEFAULT_PING_INTERVAL_MS = 25000
35
+ private static readonly DEFAULT_PONG_TIMEOUT_MS = 10000
26
36
 
27
37
  constructor(options: WebSocketConnectionOptions) {
28
38
  this.url = options.url
29
39
  this.onMessage = options.onMessage
30
40
  this.onStateChange = options.onStateChange
41
+ this.minReconnectDelay =
42
+ options.minReconnectDelay ?? WebSocketConnection.DEFAULT_MIN_RECONNECT_DELAY
43
+ this.pingIntervalMs = options.pingIntervalMs ?? WebSocketConnection.DEFAULT_PING_INTERVAL_MS
44
+ this.pongTimeoutMs = options.pongTimeoutMs ?? WebSocketConnection.DEFAULT_PONG_TIMEOUT_MS
31
45
  }
32
46
 
33
47
  get state(): ConnectionState {
@@ -64,9 +78,11 @@ export class WebSocketConnection {
64
78
  ) {
65
79
  this.ws.send(this.sendQueue.shift()!)
66
80
  }
81
+ this.startHeartbeat()
67
82
  }
68
83
 
69
84
  this.ws.onmessage = (event) => {
85
+ this.resetStaleTimer()
70
86
  try {
71
87
  const message = JSON.parse(event.data) as ServerMessage
72
88
  this.onMessage(message)
@@ -77,6 +93,7 @@ export class WebSocketConnection {
77
93
  }
78
94
 
79
95
  this.ws.onclose = () => {
96
+ this.stopHeartbeat()
80
97
  this.ws = null
81
98
  this.sendQueue.length = 0
82
99
 
@@ -104,7 +121,7 @@ export class WebSocketConnection {
104
121
  }
105
122
 
106
123
  const delay = Math.min(
107
- WebSocketConnection.MIN_RECONNECT_DELAY *
124
+ this.minReconnectDelay *
108
125
  WebSocketConnection.RECONNECT_MULTIPLIER ** this.reconnectAttempts,
109
126
  WebSocketConnection.MAX_RECONNECT_DELAY
110
127
  )
@@ -126,6 +143,8 @@ export class WebSocketConnection {
126
143
  this.reconnectTimeout = null
127
144
  }
128
145
 
146
+ this.stopHeartbeat()
147
+
129
148
  if (this.ws) {
130
149
  this.ws.close()
131
150
  this.ws = null
@@ -135,6 +154,35 @@ export class WebSocketConnection {
135
154
  this.setState('disconnected')
136
155
  }
137
156
 
157
+ private startHeartbeat() {
158
+ this.stopHeartbeat()
159
+ this.resetStaleTimer()
160
+ this.pingTimer = setInterval(() => {
161
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
162
+ this.ws.send(JSON.stringify({type: 'ping'}))
163
+ }
164
+ }, this.pingIntervalMs)
165
+ }
166
+
167
+ private stopHeartbeat() {
168
+ if (this.pingTimer) {
169
+ clearInterval(this.pingTimer)
170
+ this.pingTimer = null
171
+ }
172
+ if (this.staleTimer) {
173
+ clearTimeout(this.staleTimer)
174
+ this.staleTimer = null
175
+ }
176
+ }
177
+
178
+ private resetStaleTimer() {
179
+ if (this.staleTimer) clearTimeout(this.staleTimer)
180
+ this.staleTimer = setTimeout(() => {
181
+ debug('No frames within ping interval + pong timeout — forcing reconnect')
182
+ if (this.ws) this.ws.close()
183
+ }, this.pingIntervalMs + this.pongTimeoutMs)
184
+ }
185
+
138
186
  close() {
139
187
  this.disconnect()
140
188
  }
@@ -1,4 +1,4 @@
1
- import {WebSocketConnection} from './connection'
1
+ import {WebSocketConnection, type ConnectionState} from './connection'
2
2
  import type {
3
3
  BoundingBox,
4
4
  BoundsDeltaMessage,
@@ -22,6 +22,9 @@ export type EntityInstance = Ship | Warehouse | Container
22
22
 
23
23
  export interface SubscriptionsOptions {
24
24
  url: string
25
+ minReconnectDelay?: number
26
+ pingIntervalMs?: number
27
+ pongTimeoutMs?: number
25
28
  }
26
29
 
27
30
  export interface BoundsSubscriptionHandle {
@@ -53,6 +56,9 @@ export class SubscriptionsManager {
53
56
  private readonly boundsSubs = new Map<
54
57
  string,
55
58
  {
59
+ bounds?: BoundingBox
60
+ owner?: string
61
+ prioritizeOwner?: string
56
62
  onSnapshot?: (entities: EntityInstance[]) => void
57
63
  onUpdate?: (entity: EntityInstance) => void
58
64
  onBoundsDelta?: (entered: EntityInstance[], exited: number[]) => void
@@ -60,11 +66,16 @@ export class SubscriptionsManager {
60
66
  }
61
67
  >()
62
68
  private subCounter = 0
69
+ private hasConnected = false
63
70
 
64
71
  constructor(opts: SubscriptionsOptions) {
65
72
  this.conn = new WebSocketConnection({
66
73
  url: opts.url,
67
74
  onMessage: (m) => this.onMessage(m),
75
+ onStateChange: (s) => this.onStateChange(s),
76
+ minReconnectDelay: opts.minReconnectDelay,
77
+ pingIntervalMs: opts.pingIntervalMs,
78
+ pongTimeoutMs: opts.pongTimeoutMs,
68
79
  })
69
80
  this.conn.connect()
70
81
  }
@@ -139,6 +150,9 @@ export class SubscriptionsManager {
139
150
  current: new Map(),
140
151
  }
141
152
  this.boundsSubs.set(subId, {
153
+ bounds,
154
+ owner: handlers.owner,
155
+ prioritizeOwner: handlers.prioritizeOwner,
142
156
  onSnapshot: handlers.onSnapshot,
143
157
  onUpdate: handlers.onUpdate,
144
158
  onBoundsDelta: handlers.onBoundsDelta,
@@ -154,10 +168,39 @@ export class SubscriptionsManager {
154
168
  }
155
169
 
156
170
  private updateBounds(subId: string, bounds: BoundingBox) {
171
+ const entry = this.boundsSubs.get(subId)
172
+ if (entry) entry.bounds = bounds
157
173
  const msg: UpdateBoundsMessage = {type: 'update_bounds', sub_id: subId, bounds}
158
174
  this.sendMessage(msg)
159
175
  }
160
176
 
177
+ private onStateChange(state: ConnectionState) {
178
+ if (state !== 'connected') return
179
+ if (!this.hasConnected) {
180
+ this.hasConnected = true
181
+ return
182
+ }
183
+ for (const [subId, entry] of this.entitySubs) {
184
+ const msg: SubscribeEntityMessage = {
185
+ type: 'subscribe_entity',
186
+ sub_id: subId,
187
+ entity_type: entry.type,
188
+ entity_id: entry.id,
189
+ }
190
+ this.sendMessage(msg)
191
+ }
192
+ for (const [subId, entry] of this.boundsSubs) {
193
+ const msg: SubscribeMessage = {
194
+ type: 'subscribe',
195
+ sub_id: subId,
196
+ bounds: entry.bounds,
197
+ owner: entry.owner,
198
+ prioritize_owner: entry.prioritizeOwner,
199
+ }
200
+ this.sendMessage(msg)
201
+ }
202
+ }
203
+
161
204
  private onMessage(msg: ServerMessage) {
162
205
  switch (msg.type) {
163
206
  case 'snapshot':