@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 CHANGED
@@ -2417,6 +2417,9 @@ type SubscriptionEntityType = 'ship' | 'warehouse' | 'container';
2417
2417
  type EntityInstance = Ship | Warehouse | Container;
2418
2418
  interface SubscriptionsOptions {
2419
2419
  url: string;
2420
+ minReconnectDelay?: number;
2421
+ pingIntervalMs?: number;
2422
+ pongTimeoutMs?: number;
2420
2423
  }
2421
2424
  interface BoundsSubscriptionHandle {
2422
2425
  readonly subId: string;
@@ -2436,6 +2439,7 @@ declare class SubscriptionsManager {
2436
2439
  private readonly entitySubs;
2437
2440
  private readonly boundsSubs;
2438
2441
  private subCounter;
2442
+ private hasConnected;
2439
2443
  constructor(opts: SubscriptionsOptions);
2440
2444
  close(): void;
2441
2445
  private generateSubID;
@@ -2451,6 +2455,7 @@ declare class SubscriptionsManager {
2451
2455
  }): BoundsSubscriptionHandle;
2452
2456
  private unsubscribeBounds;
2453
2457
  private updateBounds;
2458
+ private onStateChange;
2454
2459
  private onMessage;
2455
2460
  private parseEntity;
2456
2461
  private handleSnapshot;
@@ -3098,6 +3103,9 @@ interface WebSocketConnectionOptions {
3098
3103
  url: string;
3099
3104
  onMessage: (message: ServerMessage) => void;
3100
3105
  onStateChange?: (state: ConnectionState) => void;
3106
+ minReconnectDelay?: number;
3107
+ pingIntervalMs?: number;
3108
+ pongTimeoutMs?: number;
3101
3109
  }
3102
3110
  declare class WebSocketConnection {
3103
3111
  private ws;
@@ -3109,15 +3117,25 @@ declare class WebSocketConnection {
3109
3117
  private _state;
3110
3118
  private shouldReconnect;
3111
3119
  private sendQueue;
3112
- private static readonly MIN_RECONNECT_DELAY;
3120
+ private minReconnectDelay;
3121
+ private pingIntervalMs;
3122
+ private pongTimeoutMs;
3123
+ private pingTimer;
3124
+ private staleTimer;
3125
+ private static readonly DEFAULT_MIN_RECONNECT_DELAY;
3113
3126
  private static readonly MAX_RECONNECT_DELAY;
3114
3127
  private static readonly RECONNECT_MULTIPLIER;
3128
+ private static readonly DEFAULT_PING_INTERVAL_MS;
3129
+ private static readonly DEFAULT_PONG_TIMEOUT_MS;
3115
3130
  constructor(options: WebSocketConnectionOptions);
3116
3131
  get state(): ConnectionState;
3117
3132
  private setState;
3118
3133
  connect(): void;
3119
3134
  private scheduleReconnect;
3120
3135
  disconnect(): void;
3136
+ private startHeartbeat;
3137
+ private stopHeartbeat;
3138
+ private resetStaleTimer;
3121
3139
  close(): void;
3122
3140
  send(message: ClientMessage): void;
3123
3141
  get isConnected(): boolean;
package/lib/shipload.js CHANGED
@@ -9103,9 +9103,15 @@ class WebSocketConnection {
9103
9103
  this._state = 'disconnected';
9104
9104
  this.shouldReconnect = true;
9105
9105
  this.sendQueue = [];
9106
+ this.pingTimer = null;
9107
+ this.staleTimer = null;
9106
9108
  this.url = options.url;
9107
9109
  this.onMessage = options.onMessage;
9108
9110
  this.onStateChange = options.onStateChange;
9111
+ this.minReconnectDelay =
9112
+ options.minReconnectDelay ?? WebSocketConnection.DEFAULT_MIN_RECONNECT_DELAY;
9113
+ this.pingIntervalMs = options.pingIntervalMs ?? WebSocketConnection.DEFAULT_PING_INTERVAL_MS;
9114
+ this.pongTimeoutMs = options.pongTimeoutMs ?? WebSocketConnection.DEFAULT_PONG_TIMEOUT_MS;
9109
9115
  }
9110
9116
  get state() {
9111
9117
  return this._state;
@@ -9134,8 +9140,10 @@ class WebSocketConnection {
9134
9140
  this.ws.readyState === WebSocket.OPEN) {
9135
9141
  this.ws.send(this.sendQueue.shift());
9136
9142
  }
9143
+ this.startHeartbeat();
9137
9144
  };
9138
9145
  this.ws.onmessage = (event) => {
9146
+ this.resetStaleTimer();
9139
9147
  try {
9140
9148
  const message = JSON.parse(event.data);
9141
9149
  this.onMessage(message);
@@ -9145,6 +9153,7 @@ class WebSocketConnection {
9145
9153
  }
9146
9154
  };
9147
9155
  this.ws.onclose = () => {
9156
+ this.stopHeartbeat();
9148
9157
  this.ws = null;
9149
9158
  this.sendQueue.length = 0;
9150
9159
  if (this.shouldReconnect) {
@@ -9169,7 +9178,7 @@ class WebSocketConnection {
9169
9178
  if (this.reconnectTimeout) {
9170
9179
  return;
9171
9180
  }
9172
- const delay = Math.min(WebSocketConnection.MIN_RECONNECT_DELAY *
9181
+ const delay = Math.min(this.minReconnectDelay *
9173
9182
  WebSocketConnection.RECONNECT_MULTIPLIER ** this.reconnectAttempts, WebSocketConnection.MAX_RECONNECT_DELAY);
9174
9183
  debug(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts + 1})`);
9175
9184
  this.reconnectTimeout = setTimeout(() => {
@@ -9184,6 +9193,7 @@ class WebSocketConnection {
9184
9193
  clearTimeout(this.reconnectTimeout);
9185
9194
  this.reconnectTimeout = null;
9186
9195
  }
9196
+ this.stopHeartbeat();
9187
9197
  if (this.ws) {
9188
9198
  this.ws.close();
9189
9199
  this.ws = null;
@@ -9191,6 +9201,34 @@ class WebSocketConnection {
9191
9201
  this.sendQueue.length = 0;
9192
9202
  this.setState('disconnected');
9193
9203
  }
9204
+ startHeartbeat() {
9205
+ this.stopHeartbeat();
9206
+ this.resetStaleTimer();
9207
+ this.pingTimer = setInterval(() => {
9208
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
9209
+ this.ws.send(JSON.stringify({ type: 'ping' }));
9210
+ }
9211
+ }, this.pingIntervalMs);
9212
+ }
9213
+ stopHeartbeat() {
9214
+ if (this.pingTimer) {
9215
+ clearInterval(this.pingTimer);
9216
+ this.pingTimer = null;
9217
+ }
9218
+ if (this.staleTimer) {
9219
+ clearTimeout(this.staleTimer);
9220
+ this.staleTimer = null;
9221
+ }
9222
+ }
9223
+ resetStaleTimer() {
9224
+ if (this.staleTimer)
9225
+ clearTimeout(this.staleTimer);
9226
+ this.staleTimer = setTimeout(() => {
9227
+ debug('No frames within ping interval + pong timeout — forcing reconnect');
9228
+ if (this.ws)
9229
+ this.ws.close();
9230
+ }, this.pingIntervalMs + this.pongTimeoutMs);
9231
+ }
9194
9232
  close() {
9195
9233
  this.disconnect();
9196
9234
  }
@@ -9206,9 +9244,11 @@ class WebSocketConnection {
9206
9244
  return this._state === 'connected';
9207
9245
  }
9208
9246
  }
9209
- WebSocketConnection.MIN_RECONNECT_DELAY = 1000;
9247
+ WebSocketConnection.DEFAULT_MIN_RECONNECT_DELAY = 1000;
9210
9248
  WebSocketConnection.MAX_RECONNECT_DELAY = 30000;
9211
9249
  WebSocketConnection.RECONNECT_MULTIPLIER = 2;
9250
+ WebSocketConnection.DEFAULT_PING_INTERVAL_MS = 25000;
9251
+ WebSocketConnection.DEFAULT_PONG_TIMEOUT_MS = 10000;
9212
9252
 
9213
9253
  function mapEntity(ei) {
9214
9254
  if (ei.type.equals('ship'))
@@ -9237,9 +9277,14 @@ class SubscriptionsManager {
9237
9277
  this.entitySubs = new Map();
9238
9278
  this.boundsSubs = new Map();
9239
9279
  this.subCounter = 0;
9280
+ this.hasConnected = false;
9240
9281
  this.conn = new WebSocketConnection({
9241
9282
  url: opts.url,
9242
9283
  onMessage: (m) => this.onMessage(m),
9284
+ onStateChange: (s) => this.onStateChange(s),
9285
+ minReconnectDelay: opts.minReconnectDelay,
9286
+ pingIntervalMs: opts.pingIntervalMs,
9287
+ pongTimeoutMs: opts.pongTimeoutMs,
9243
9288
  });
9244
9289
  this.conn.connect();
9245
9290
  }
@@ -9296,6 +9341,9 @@ class SubscriptionsManager {
9296
9341
  current: new Map(),
9297
9342
  };
9298
9343
  this.boundsSubs.set(subId, {
9344
+ bounds,
9345
+ owner: handlers.owner,
9346
+ prioritizeOwner: handlers.prioritizeOwner,
9299
9347
  onSnapshot: handlers.onSnapshot,
9300
9348
  onUpdate: handlers.onUpdate,
9301
9349
  onBoundsDelta: handlers.onBoundsDelta,
@@ -9309,9 +9357,39 @@ class SubscriptionsManager {
9309
9357
  this.sendMessage({ type: 'unsubscribe', sub_id: subId });
9310
9358
  }
9311
9359
  updateBounds(subId, bounds) {
9360
+ const entry = this.boundsSubs.get(subId);
9361
+ if (entry)
9362
+ entry.bounds = bounds;
9312
9363
  const msg = { type: 'update_bounds', sub_id: subId, bounds };
9313
9364
  this.sendMessage(msg);
9314
9365
  }
9366
+ onStateChange(state) {
9367
+ if (state !== 'connected')
9368
+ return;
9369
+ if (!this.hasConnected) {
9370
+ this.hasConnected = true;
9371
+ return;
9372
+ }
9373
+ for (const [subId, entry] of this.entitySubs) {
9374
+ const msg = {
9375
+ type: 'subscribe_entity',
9376
+ sub_id: subId,
9377
+ entity_type: entry.type,
9378
+ entity_id: entry.id,
9379
+ };
9380
+ this.sendMessage(msg);
9381
+ }
9382
+ for (const [subId, entry] of this.boundsSubs) {
9383
+ const msg = {
9384
+ type: 'subscribe',
9385
+ sub_id: subId,
9386
+ bounds: entry.bounds,
9387
+ owner: entry.owner,
9388
+ prioritize_owner: entry.prioritizeOwner,
9389
+ };
9390
+ this.sendMessage(msg);
9391
+ }
9392
+ }
9315
9393
  onMessage(msg) {
9316
9394
  switch (msg.type) {
9317
9395
  case 'snapshot':