skikrumb-api 2.1.20 → 2.2.0

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/dist/index.d.ts CHANGED
@@ -8,21 +8,34 @@ declare class SkiKrumbRealtimeClient {
8
8
  private userId;
9
9
  private endpointType;
10
10
  private listeners;
11
- private isConnecting;
12
11
  private reconnectAttempts;
13
12
  private maxReconnectAttempts;
14
13
  private reconnectDelay;
15
14
  private maxReconnectDelay;
15
+ private reconnectTimer;
16
+ private manuallyDisconnected;
17
+ private connectPromise;
18
+ private connectReject;
19
+ private connectTimeoutTimer;
20
+ private connectTimeout;
21
+ private heartbeatTimer;
22
+ private pongTimer;
23
+ private heartbeatIntervalMs;
24
+ private pongTimeoutMs;
16
25
  constructor(userId: string, url: string, endpointType: RealtimeEndpointType, sessionToken?: string, supabaseToken?: string);
17
26
  connect(): Promise<void>;
18
27
  private connectToWebSocket;
19
28
  private scheduleReconnect;
29
+ private resetHeartbeat;
30
+ private clearHeartbeat;
20
31
  disconnect(): void;
21
- ping(): void;
22
- sendPong(): void;
23
- requestRefresh(): void;
32
+ destroy(): void;
33
+ ping(): boolean;
34
+ sendPong(): boolean;
35
+ requestRefresh(): boolean;
24
36
  on(event: string, callback: Function): void;
25
37
  off(event: string, callback?: Function): void;
38
+ removeAllListeners(): void;
26
39
  private emit;
27
40
  isConnected(): boolean;
28
41
  }
@@ -41,6 +54,9 @@ export declare const skiKrumb: (options?: {
41
54
  readOrganizationData: () => Promise<Data[]>;
42
55
  readGateways: () => Promise<Gateways>;
43
56
  readShippingRates: (rateRequest: RateRequest) => Promise<Rates>;
57
+ sendLogs: (payload: {
58
+ logs: any[];
59
+ }) => Promise<unknown>;
44
60
  sendMobileLocation: (payload: {
45
61
  deviceId: string;
46
62
  locations: any[];
package/dist/index.js CHANGED
@@ -3,11 +3,20 @@ class SkiKrumbRealtimeClient {
3
3
  constructor(userId, url, endpointType, sessionToken, supabaseToken) {
4
4
  this.websocket = null;
5
5
  this.listeners = {};
6
- this.isConnecting = false;
7
6
  this.reconnectAttempts = 0;
8
7
  this.maxReconnectAttempts = Infinity; // Never stop trying
9
8
  this.reconnectDelay = 1000; // 1 second base
10
9
  this.maxReconnectDelay = 300000; // 5 minutes max
10
+ this.reconnectTimer = null;
11
+ this.manuallyDisconnected = false;
12
+ this.connectPromise = null;
13
+ this.connectReject = null;
14
+ this.connectTimeoutTimer = null;
15
+ this.connectTimeout = 15000; // 15 second connect handshake timeout
16
+ this.heartbeatTimer = null;
17
+ this.pongTimer = null;
18
+ this.heartbeatIntervalMs = 30000; // Send ping after 30s silence
19
+ this.pongTimeoutMs = 10000; // Expect response within 10s
11
20
  this.userId = userId;
12
21
  this.url = url.replace(/\/+$/, ''); // Remove trailing slashes
13
22
  this.endpointType = endpointType;
@@ -15,29 +24,40 @@ class SkiKrumbRealtimeClient {
15
24
  this.supabaseToken = supabaseToken;
16
25
  }
17
26
  async connect() {
18
- if (this.isConnecting) {
19
- return Promise.resolve();
27
+ this.manuallyDisconnected = false;
28
+ if (this.reconnectTimer) {
29
+ clearTimeout(this.reconnectTimer);
30
+ this.reconnectTimer = null;
20
31
  }
21
- if (this.websocket && this.websocket.readyState === WebSocket.OPEN) {
22
- return Promise.resolve();
32
+ if (this.connectPromise) {
33
+ return this.connectPromise;
23
34
  }
24
- // Additional guard: check if we're already connecting or connected
25
35
  if (this.websocket &&
26
36
  (this.websocket.readyState === WebSocket.CONNECTING ||
27
37
  this.websocket.readyState === WebSocket.OPEN)) {
28
- return Promise.resolve();
38
+ return;
29
39
  }
30
40
  // Clean up any stale websocket before creating new one
41
+ // Don't use disconnect() here — it sets manuallyDisconnected = true,
42
+ // which would prevent auto-reconnect if the subsequent connection fails.
31
43
  if (this.websocket && this.websocket.readyState >= WebSocket.CLOSING) {
32
- this.disconnect();
44
+ this.clearHeartbeat();
45
+ this.websocket.onopen = null;
46
+ this.websocket.onmessage = null;
47
+ this.websocket.onerror = null;
48
+ this.websocket.onclose = null;
49
+ this.websocket = null;
33
50
  }
34
- this.isConnecting = true;
51
+ const promise = this.connectToWebSocket();
52
+ this.connectPromise = promise;
35
53
  try {
36
- await this.connectToWebSocket();
54
+ await promise;
37
55
  }
38
- catch (error) {
39
- this.isConnecting = false;
40
- throw error;
56
+ finally {
57
+ // Only null if this is still OUR promise (not a newer connect's)
58
+ if (this.connectPromise === promise) {
59
+ this.connectPromise = null;
60
+ }
41
61
  }
42
62
  }
43
63
  connectToWebSocket() {
@@ -55,6 +75,18 @@ class SkiKrumbRealtimeClient {
55
75
  return reject(new Error('Session token is required for user endpoint.'));
56
76
  }
57
77
  wsUrl = `${baseWsUrl}${endpointPath}?token=${encodeURIComponent(this.sessionToken)}&supabaseToken=${encodeURIComponent(this.supabaseToken)}`;
78
+ this.connectReject = reject;
79
+ // Start connect timeout — safety net if server never sends auth_success
80
+ this.connectTimeoutTimer = setTimeout(() => {
81
+ var _a;
82
+ this.connectTimeoutTimer = null;
83
+ if (this.connectReject) {
84
+ const pendingReject = this.connectReject;
85
+ this.connectReject = null;
86
+ (_a = this.websocket) === null || _a === void 0 ? void 0 : _a.close();
87
+ pendingReject(new Error('connect_timeout'));
88
+ }
89
+ }, this.connectTimeout);
58
90
  this.websocket = new WebSocket(wsUrl);
59
91
  this.websocket.onopen = () => {
60
92
  // Authentication is handled automatically via query parameters
@@ -63,15 +95,25 @@ class SkiKrumbRealtimeClient {
63
95
  this.websocket.onmessage = (event) => {
64
96
  var _a, _b, _c, _d, _e, _f;
65
97
  try {
98
+ this.resetHeartbeat();
66
99
  const message = JSON.parse(event.data);
67
100
  if (message.type === 'auth_success') {
68
- this.isConnecting = false;
69
101
  this.reconnectAttempts = 0;
102
+ this.connectReject = null;
103
+ if (this.connectTimeoutTimer) {
104
+ clearTimeout(this.connectTimeoutTimer);
105
+ this.connectTimeoutTimer = null;
106
+ }
70
107
  this.emit('connected', message);
71
108
  resolve();
72
109
  }
73
110
  else if (message.type === 'auth_error' ||
74
111
  message.type === 'error') {
112
+ this.connectReject = null;
113
+ if (this.connectTimeoutTimer) {
114
+ clearTimeout(this.connectTimeoutTimer);
115
+ this.connectTimeoutTimer = null;
116
+ }
75
117
  (_a = this.websocket) === null || _a === void 0 ? void 0 : _a.close();
76
118
  reject(new Error(`Connection failed: ${message.message || message.error}`));
77
119
  }
@@ -123,50 +165,112 @@ class SkiKrumbRealtimeClient {
123
165
  }
124
166
  };
125
167
  this.websocket.onerror = (error) => {
126
- this.isConnecting = false;
168
+ this.connectReject = null;
169
+ if (this.connectTimeoutTimer) {
170
+ clearTimeout(this.connectTimeoutTimer);
171
+ this.connectTimeoutTimer = null;
172
+ }
127
173
  this.emit('error', error);
128
174
  reject(error);
129
175
  };
130
176
  this.websocket.onclose = (event) => {
177
+ this.clearHeartbeat();
131
178
  this.websocket = null;
132
- this.isConnecting = false;
133
- // Don't auto-reconnect if it's a session replacement or normal close
134
- if (event.code === 4000) {
135
- this.emit('disconnected', { reason: 'Session replaced' });
179
+ const pendingReject = this.connectReject;
180
+ this.connectReject = null;
181
+ if (this.connectTimeoutTimer) {
182
+ clearTimeout(this.connectTimeoutTimer);
183
+ this.connectTimeoutTimer = null;
136
184
  }
137
- else if (event.code === 1000) {
138
- this.emit('disconnected', { reason: 'Manual disconnect' });
185
+ if (event.code === 4000 || event.reason === 'New connection established') {
186
+ this.emit('session_replaced', {
187
+ reason: event.reason || 'Session replaced',
188
+ });
189
+ this.emit('disconnected', { reason: 'Session replaced' });
139
190
  }
140
191
  else {
141
- // Only reconnect for unexpected disconnections
142
192
  this.emit('disconnected', {
143
193
  reason: event.reason || 'Connection closed',
144
194
  });
145
- // Always try to reconnect with smart backoff
146
- this.scheduleReconnect();
195
+ if (event.code !== 1000) {
196
+ this.scheduleReconnect();
197
+ }
147
198
  }
199
+ // Reject pending connect promise if onclose fired before auth_success
200
+ pendingReject === null || pendingReject === void 0 ? void 0 : pendingReject(new Error(event.reason || 'Connection closed before auth'));
148
201
  };
149
202
  }
150
203
  catch (error) {
151
- this.isConnecting = false;
204
+ this.connectReject = null;
205
+ if (this.connectTimeoutTimer) {
206
+ clearTimeout(this.connectTimeoutTimer);
207
+ this.connectTimeoutTimer = null;
208
+ }
152
209
  reject(error);
153
210
  }
154
211
  });
155
212
  }
156
213
  scheduleReconnect() {
214
+ if (this.manuallyDisconnected)
215
+ return;
157
216
  this.reconnectAttempts++;
158
- // Exponential backoff with 5-minute cap
159
- const delay = Math.min(this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1), this.maxReconnectDelay);
160
- setTimeout(() => {
161
- this.connect().catch((error) => {
217
+ // Exponential backoff with 5-minute cap and jitter to prevent thundering herd
218
+ const base = Math.min(this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1), this.maxReconnectDelay);
219
+ const delay = Math.round(base * (0.5 + Math.random() * 0.5));
220
+ this.reconnectTimer = setTimeout(() => {
221
+ this.reconnectTimer = null;
222
+ this.connect().catch(() => {
162
223
  // Errors are expected during reconnection, will keep trying
163
- // No max attempts check - keeps trying indefinitely with smart backoff
164
224
  });
165
225
  }, delay);
166
226
  }
227
+ resetHeartbeat() {
228
+ var _a;
229
+ this.clearHeartbeat();
230
+ if (((_a = this.websocket) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN) {
231
+ this.heartbeatTimer = setTimeout(() => {
232
+ this.heartbeatTimer = null;
233
+ // No messages received for 30s — probe the connection
234
+ if (this.ping()) {
235
+ this.pongTimer = setTimeout(() => {
236
+ var _a;
237
+ this.pongTimer = null;
238
+ // No response to probe — connection is dead
239
+ (_a = this.websocket) === null || _a === void 0 ? void 0 : _a.close(4001, 'Heartbeat timeout');
240
+ }, this.pongTimeoutMs);
241
+ }
242
+ }, this.heartbeatIntervalMs);
243
+ }
244
+ }
245
+ clearHeartbeat() {
246
+ if (this.heartbeatTimer) {
247
+ clearTimeout(this.heartbeatTimer);
248
+ this.heartbeatTimer = null;
249
+ }
250
+ if (this.pongTimer) {
251
+ clearTimeout(this.pongTimer);
252
+ this.pongTimer = null;
253
+ }
254
+ }
167
255
  disconnect() {
168
- this.isConnecting = false;
256
+ this.manuallyDisconnected = true;
257
+ this.connectPromise = null;
169
258
  this.reconnectAttempts = 0;
259
+ this.clearHeartbeat();
260
+ if (this.reconnectTimer) {
261
+ clearTimeout(this.reconnectTimer);
262
+ this.reconnectTimer = null;
263
+ }
264
+ // Reject pending connect promise so await connect() throws
265
+ if (this.connectReject) {
266
+ const pendingReject = this.connectReject;
267
+ this.connectReject = null;
268
+ if (this.connectTimeoutTimer) {
269
+ clearTimeout(this.connectTimeoutTimer);
270
+ this.connectTimeoutTimer = null;
271
+ }
272
+ pendingReject(new Error('disconnect() called'));
273
+ }
170
274
  if (this.websocket) {
171
275
  // Remove all listeners to prevent events during cleanup
172
276
  this.websocket.onopen = null;
@@ -181,15 +285,23 @@ class SkiKrumbRealtimeClient {
181
285
  this.websocket = null;
182
286
  }
183
287
  }
288
+ destroy() {
289
+ this.disconnect();
290
+ this.removeAllListeners();
291
+ }
184
292
  ping() {
185
293
  if (this.websocket && this.websocket.readyState === WebSocket.OPEN) {
186
294
  this.websocket.send(JSON.stringify({ type: 'ping', timestamp: new Date().toISOString() }));
295
+ return true;
187
296
  }
297
+ return false;
188
298
  }
189
299
  sendPong() {
190
300
  if (this.websocket && this.websocket.readyState === WebSocket.OPEN) {
191
301
  this.websocket.send(JSON.stringify({ type: 'pong', timestamp: new Date().toISOString() }));
302
+ return true;
192
303
  }
304
+ return false;
193
305
  }
194
306
  requestRefresh() {
195
307
  if (this.websocket && this.websocket.readyState === WebSocket.OPEN) {
@@ -197,7 +309,9 @@ class SkiKrumbRealtimeClient {
197
309
  type: 'request_refresh',
198
310
  timestamp: new Date().toISOString(),
199
311
  }));
312
+ return true;
200
313
  }
314
+ return false;
201
315
  }
202
316
  on(event, callback) {
203
317
  if (!this.listeners[event]) {
@@ -215,13 +329,18 @@ class SkiKrumbRealtimeClient {
215
329
  this.listeners[event] = [];
216
330
  }
217
331
  }
332
+ removeAllListeners() {
333
+ this.listeners = {};
334
+ }
218
335
  emit(event, data) {
219
336
  const callbacks = this.listeners[event] || [];
220
337
  callbacks.forEach((callback) => {
221
338
  try {
222
339
  callback(data);
223
340
  }
224
- catch (error) { }
341
+ catch (error) {
342
+ console.warn(`[SkiKrumbRealtime] Error in "${event}" listener:`, error);
343
+ }
225
344
  });
226
345
  }
227
346
  isConnected() {
@@ -353,6 +472,12 @@ export const skiKrumb = (options = {
353
472
  throw error;
354
473
  }
355
474
  };
475
+ const sendLogs = async (payload) => {
476
+ const response = await request
477
+ .post('devices/logs', { json: payload })
478
+ .json();
479
+ return response;
480
+ };
356
481
  const sendMobileLocation = async (payload) => {
357
482
  const response = await request
358
483
  .post('devices/location/mobile', { json: payload })
@@ -680,6 +805,7 @@ export const skiKrumb = (options = {
680
805
  readOrganizationData,
681
806
  readGateways,
682
807
  readShippingRates,
808
+ sendLogs,
683
809
  sendMobileLocation,
684
810
  authenticateExternalUser,
685
811
  refreshSessionToken,
package/dist/models.d.ts CHANGED
@@ -109,6 +109,7 @@ export interface ExternalUserResponse {
109
109
  success: boolean;
110
110
  accountId?: string;
111
111
  sessionToken?: string;
112
+ supabaseToken?: string;
112
113
  isNewUser?: boolean;
113
114
  error?: string;
114
115
  accessibleSerials?: string[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skikrumb-api",
3
- "version": "2.1.20",
3
+ "version": "2.2.0",
4
4
  "description": "Wrapper for the skiKrumb API",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -20,5 +20,6 @@
20
20
  },
21
21
  "dependencies": {
22
22
  "ky": "^1.7.5"
23
- }
23
+ },
24
+ "packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610"
24
25
  }
@@ -1,11 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Read(//Users/michael/.config/nvim/**)",
5
- "Read(//Users/michael/.config/**)",
6
- "Read(//Users/michael/Code/skikrumb-api/**)"
7
- ],
8
- "deny": [],
9
- "ask": []
10
- }
11
- }