@shipload/sdk 1.0.0-next.0 → 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/lib/shipload.d.ts +19 -1
- package/lib/shipload.js +80 -2
- package/lib/shipload.js.map +1 -1
- package/lib/shipload.m.js +80 -2
- package/lib/shipload.m.js.map +1 -1
- package/package.json +7 -2
- package/src/subscriptions/connection.ts +50 -2
- package/src/subscriptions/manager.ts +44 -1
package/package.json
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipload/sdk",
|
|
3
3
|
"description": "SDKs for Shipload",
|
|
4
|
-
"version": "1.0.0-next.
|
|
5
|
-
"homepage": "https://github.com/shipload/sdk",
|
|
4
|
+
"version": "1.0.0-next.2",
|
|
5
|
+
"homepage": "https://github.com/shipload/toolkit/tree/master/packages/sdk",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/shipload/toolkit.git",
|
|
9
|
+
"directory": "packages/sdk"
|
|
10
|
+
},
|
|
6
11
|
"license": "MIT",
|
|
7
12
|
"main": "lib/shipload.js",
|
|
8
13
|
"module": "lib/shipload.m.js",
|
|
@@ -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
|
|
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
|
-
|
|
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':
|