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 +20 -4
- package/dist/index.js +158 -32
- package/dist/models.d.ts +1 -0
- package/package.json +3 -2
- package/.claude/settings.local.json +0 -11
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
19
|
-
|
|
27
|
+
this.manuallyDisconnected = false;
|
|
28
|
+
if (this.reconnectTimer) {
|
|
29
|
+
clearTimeout(this.reconnectTimer);
|
|
30
|
+
this.reconnectTimer = null;
|
|
20
31
|
}
|
|
21
|
-
if (this.
|
|
22
|
-
return
|
|
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
|
|
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.
|
|
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
|
-
|
|
51
|
+
const promise = this.connectToWebSocket();
|
|
52
|
+
this.connectPromise = promise;
|
|
35
53
|
try {
|
|
36
|
-
await
|
|
54
|
+
await promise;
|
|
37
55
|
}
|
|
38
|
-
|
|
39
|
-
this
|
|
40
|
-
|
|
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.
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
if (
|
|
135
|
-
this.
|
|
179
|
+
const pendingReject = this.connectReject;
|
|
180
|
+
this.connectReject = null;
|
|
181
|
+
if (this.connectTimeoutTimer) {
|
|
182
|
+
clearTimeout(this.connectTimeoutTimer);
|
|
183
|
+
this.connectTimeoutTimer = null;
|
|
136
184
|
}
|
|
137
|
-
|
|
138
|
-
this.emit('
|
|
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
|
-
|
|
146
|
-
|
|
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.
|
|
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
|
|
160
|
-
|
|
161
|
-
|
|
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.
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skikrumb-api",
|
|
3
|
-
"version": "2.
|
|
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
|
}
|