@pushflodev/sdk 1.0.1
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/LICENSE +21 -0
- package/README.md +500 -0
- package/dist/PushFloClient-JPowShqE.d.cts +102 -0
- package/dist/PushFloClient-ny0iyTYJ.d.ts +102 -0
- package/dist/api-DYrG_5uC.d.cts +149 -0
- package/dist/api-DYrG_5uC.d.ts +149 -0
- package/dist/index.cjs +1210 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +28 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +1204 -0
- package/dist/index.js.map +1 -0
- package/dist/message-CVgilLwz.d.cts +130 -0
- package/dist/message-CVgilLwz.d.ts +130 -0
- package/dist/react.cjs +1300 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +116 -0
- package/dist/react.d.ts +116 -0
- package/dist/react.js +1295 -0
- package/dist/react.js.map +1 -0
- package/dist/server.cjs +558 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +47 -0
- package/dist/server.d.ts +47 -0
- package/dist/server.js +553 -0
- package/dist/server.js.map +1 -0
- package/package.json +103 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1204 @@
|
|
|
1
|
+
/* @pushflo/sdk - https://pushflo.dev */
|
|
2
|
+
|
|
3
|
+
// src/utils/EventEmitter.ts
|
|
4
|
+
var TypedEventEmitter = class {
|
|
5
|
+
listeners = /* @__PURE__ */ new Map();
|
|
6
|
+
/**
|
|
7
|
+
* Register an event listener
|
|
8
|
+
*/
|
|
9
|
+
on(event, handler) {
|
|
10
|
+
if (!this.listeners.has(event)) {
|
|
11
|
+
this.listeners.set(event, /* @__PURE__ */ new Set());
|
|
12
|
+
}
|
|
13
|
+
this.listeners.get(event).add(handler);
|
|
14
|
+
return this;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Register a one-time event listener
|
|
18
|
+
*/
|
|
19
|
+
once(event, handler) {
|
|
20
|
+
const onceHandler = (...args) => {
|
|
21
|
+
this.off(event, onceHandler);
|
|
22
|
+
handler(...args);
|
|
23
|
+
};
|
|
24
|
+
return this.on(event, onceHandler);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Remove an event listener
|
|
28
|
+
*/
|
|
29
|
+
off(event, handler) {
|
|
30
|
+
const handlers = this.listeners.get(event);
|
|
31
|
+
if (handlers) {
|
|
32
|
+
handlers.delete(handler);
|
|
33
|
+
if (handlers.size === 0) {
|
|
34
|
+
this.listeners.delete(event);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return this;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Emit an event to all registered listeners
|
|
41
|
+
*/
|
|
42
|
+
emit(event, ...args) {
|
|
43
|
+
const handlers = this.listeners.get(event);
|
|
44
|
+
if (!handlers || handlers.size === 0) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
handlers.forEach((handler) => {
|
|
48
|
+
try {
|
|
49
|
+
handler(...args);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error(`Error in event handler for "${String(event)}":`, error);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Remove all listeners for an event, or all listeners if no event specified
|
|
58
|
+
*/
|
|
59
|
+
removeAllListeners(event) {
|
|
60
|
+
if (event !== void 0) {
|
|
61
|
+
this.listeners.delete(event);
|
|
62
|
+
} else {
|
|
63
|
+
this.listeners.clear();
|
|
64
|
+
}
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Get the number of listeners for an event
|
|
69
|
+
*/
|
|
70
|
+
listenerCount(event) {
|
|
71
|
+
return this.listeners.get(event)?.size ?? 0;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Get all event names with registered listeners
|
|
75
|
+
*/
|
|
76
|
+
eventNames() {
|
|
77
|
+
return Array.from(this.listeners.keys());
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// src/utils/constants.ts
|
|
82
|
+
var DEFAULTS = {
|
|
83
|
+
/** Default PushFlo API base URL */
|
|
84
|
+
BASE_URL: "https://api.pushflo.dev",
|
|
85
|
+
/** WebSocket endpoint path */
|
|
86
|
+
WS_PATH: "/ws",
|
|
87
|
+
/** Connection timeout in milliseconds */
|
|
88
|
+
CONNECTION_TIMEOUT: 3e4,
|
|
89
|
+
/** Heartbeat interval in milliseconds */
|
|
90
|
+
HEARTBEAT_INTERVAL: 25e3,
|
|
91
|
+
/** Initial reconnection delay in milliseconds */
|
|
92
|
+
RECONNECT_DELAY: 1e3,
|
|
93
|
+
/** Maximum reconnection delay in milliseconds */
|
|
94
|
+
MAX_RECONNECT_DELAY: 3e4,
|
|
95
|
+
/** Reconnection delay multiplier for exponential backoff */
|
|
96
|
+
RECONNECT_MULTIPLIER: 1.5,
|
|
97
|
+
/** Maximum number of reconnection attempts (0 = infinite) */
|
|
98
|
+
MAX_RECONNECT_ATTEMPTS: 0};
|
|
99
|
+
var WS_CLIENT_MESSAGES = {
|
|
100
|
+
SUBSCRIBE: "subscribe",
|
|
101
|
+
UNSUBSCRIBE: "unsubscribe",
|
|
102
|
+
PING: "ping"};
|
|
103
|
+
var WS_SERVER_MESSAGES = {
|
|
104
|
+
CONNECTED: "connected",
|
|
105
|
+
SUBSCRIBED: "subscribed",
|
|
106
|
+
UNSUBSCRIBED: "unsubscribed",
|
|
107
|
+
MESSAGE: "message",
|
|
108
|
+
ERROR: "error",
|
|
109
|
+
PONG: "pong"
|
|
110
|
+
};
|
|
111
|
+
var ERROR_CODES = {
|
|
112
|
+
// Connection errors
|
|
113
|
+
CONNECTION_FAILED: "CONNECTION_FAILED",
|
|
114
|
+
CONNECTION_TIMEOUT: "CONNECTION_TIMEOUT",
|
|
115
|
+
CONNECTION_CLOSED: "CONNECTION_CLOSED",
|
|
116
|
+
// Authentication errors
|
|
117
|
+
INVALID_API_KEY: "INVALID_API_KEY",
|
|
118
|
+
UNAUTHORIZED: "UNAUTHORIZED",
|
|
119
|
+
FORBIDDEN: "FORBIDDEN",
|
|
120
|
+
// Network errors
|
|
121
|
+
NETWORK_ERROR: "NETWORK_ERROR",
|
|
122
|
+
REQUEST_TIMEOUT: "REQUEST_TIMEOUT",
|
|
123
|
+
// API errors
|
|
124
|
+
NOT_FOUND: "NOT_FOUND",
|
|
125
|
+
VALIDATION_ERROR: "VALIDATION_ERROR",
|
|
126
|
+
RATE_LIMITED: "RATE_LIMITED",
|
|
127
|
+
SERVER_ERROR: "SERVER_ERROR"};
|
|
128
|
+
|
|
129
|
+
// src/utils/logger.ts
|
|
130
|
+
var LOG_LEVELS = {
|
|
131
|
+
debug: 0,
|
|
132
|
+
info: 1,
|
|
133
|
+
warn: 2,
|
|
134
|
+
error: 3
|
|
135
|
+
};
|
|
136
|
+
var Logger = class {
|
|
137
|
+
enabled;
|
|
138
|
+
prefix;
|
|
139
|
+
minLevel;
|
|
140
|
+
constructor(options = {}) {
|
|
141
|
+
this.enabled = options.debug ?? false;
|
|
142
|
+
this.prefix = options.prefix ?? "[PushFlo]";
|
|
143
|
+
this.minLevel = LOG_LEVELS[options.level ?? "debug"];
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Enable or disable logging
|
|
147
|
+
*/
|
|
148
|
+
setEnabled(enabled) {
|
|
149
|
+
this.enabled = enabled;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Set minimum log level
|
|
153
|
+
*/
|
|
154
|
+
setLevel(level) {
|
|
155
|
+
this.minLevel = LOG_LEVELS[level];
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Log a debug message
|
|
159
|
+
*/
|
|
160
|
+
debug(message, ...args) {
|
|
161
|
+
this.log("debug", message, ...args);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Log an info message
|
|
165
|
+
*/
|
|
166
|
+
info(message, ...args) {
|
|
167
|
+
this.log("info", message, ...args);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Log a warning message
|
|
171
|
+
*/
|
|
172
|
+
warn(message, ...args) {
|
|
173
|
+
this.log("warn", message, ...args);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Log an error message
|
|
177
|
+
*/
|
|
178
|
+
error(message, ...args) {
|
|
179
|
+
this.log("error", message, ...args);
|
|
180
|
+
}
|
|
181
|
+
log(level, message, ...args) {
|
|
182
|
+
if (!this.enabled && level !== "error") {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (LOG_LEVELS[level] < this.minLevel) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
189
|
+
const formattedMessage = `${this.prefix} ${timestamp} [${level.toUpperCase()}] ${message}`;
|
|
190
|
+
switch (level) {
|
|
191
|
+
case "debug":
|
|
192
|
+
console.debug(formattedMessage, ...args);
|
|
193
|
+
break;
|
|
194
|
+
case "info":
|
|
195
|
+
console.info(formattedMessage, ...args);
|
|
196
|
+
break;
|
|
197
|
+
case "warn":
|
|
198
|
+
console.warn(formattedMessage, ...args);
|
|
199
|
+
break;
|
|
200
|
+
case "error":
|
|
201
|
+
console.error(formattedMessage, ...args);
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
function createLogger(options = {}) {
|
|
207
|
+
return new Logger(options);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// src/errors/PushFloError.ts
|
|
211
|
+
var PushFloError = class extends Error {
|
|
212
|
+
/** Error code for programmatic handling */
|
|
213
|
+
code;
|
|
214
|
+
/** Whether this error is potentially recoverable through retry */
|
|
215
|
+
retryable;
|
|
216
|
+
/** Original error that caused this error, if any */
|
|
217
|
+
cause;
|
|
218
|
+
constructor(message, code, options = {}) {
|
|
219
|
+
super(message);
|
|
220
|
+
this.name = "PushFloError";
|
|
221
|
+
this.code = code;
|
|
222
|
+
this.retryable = options.retryable ?? false;
|
|
223
|
+
this.cause = options.cause;
|
|
224
|
+
if (Error.captureStackTrace) {
|
|
225
|
+
Error.captureStackTrace(this, this.constructor);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Convert error to JSON-serializable object
|
|
230
|
+
*/
|
|
231
|
+
toJSON() {
|
|
232
|
+
return {
|
|
233
|
+
name: this.name,
|
|
234
|
+
message: this.message,
|
|
235
|
+
code: this.code,
|
|
236
|
+
retryable: this.retryable,
|
|
237
|
+
stack: this.stack,
|
|
238
|
+
cause: this.cause?.message
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Create a string representation
|
|
243
|
+
*/
|
|
244
|
+
toString() {
|
|
245
|
+
return `${this.name} [${this.code}]: ${this.message}`;
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
// src/errors/AuthenticationError.ts
|
|
250
|
+
var AuthenticationError = class _AuthenticationError extends PushFloError {
|
|
251
|
+
constructor(message, code = ERROR_CODES.UNAUTHORIZED, options = {}) {
|
|
252
|
+
super(message, code, { retryable: false, ...options });
|
|
253
|
+
this.name = "AuthenticationError";
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Create an invalid API key error
|
|
257
|
+
*/
|
|
258
|
+
static invalidKey(keyType) {
|
|
259
|
+
const message = keyType ? `Invalid ${keyType} API key` : "Invalid API key";
|
|
260
|
+
return new _AuthenticationError(message, ERROR_CODES.INVALID_API_KEY);
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Create an unauthorized error
|
|
264
|
+
*/
|
|
265
|
+
static unauthorized(reason) {
|
|
266
|
+
return new _AuthenticationError(
|
|
267
|
+
reason ?? "Unauthorized - check your API key",
|
|
268
|
+
ERROR_CODES.UNAUTHORIZED
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Create a forbidden error
|
|
273
|
+
*/
|
|
274
|
+
static forbidden(action) {
|
|
275
|
+
const message = action ? `Access forbidden: insufficient permissions for ${action}` : "Access forbidden: insufficient permissions";
|
|
276
|
+
return new _AuthenticationError(message, ERROR_CODES.FORBIDDEN);
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
// src/utils/retry.ts
|
|
281
|
+
function calculateBackoff(attempt, options = {}) {
|
|
282
|
+
const {
|
|
283
|
+
initialDelay = DEFAULTS.RECONNECT_DELAY,
|
|
284
|
+
maxDelay = DEFAULTS.MAX_RECONNECT_DELAY,
|
|
285
|
+
multiplier = DEFAULTS.RECONNECT_MULTIPLIER
|
|
286
|
+
} = options;
|
|
287
|
+
const exponentialDelay = initialDelay * Math.pow(multiplier, attempt);
|
|
288
|
+
const cappedDelay = Math.min(exponentialDelay, maxDelay);
|
|
289
|
+
const jitter = cappedDelay * 0.25 * (Math.random() * 2 - 1);
|
|
290
|
+
return Math.floor(cappedDelay + jitter);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// src/errors/ConnectionError.ts
|
|
294
|
+
var ConnectionError = class _ConnectionError extends PushFloError {
|
|
295
|
+
constructor(message, code = ERROR_CODES.CONNECTION_FAILED, options = {}) {
|
|
296
|
+
super(message, code, { retryable: true, ...options });
|
|
297
|
+
this.name = "ConnectionError";
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Create a connection timeout error
|
|
301
|
+
*/
|
|
302
|
+
static timeout(timeoutMs) {
|
|
303
|
+
return new _ConnectionError(
|
|
304
|
+
`Connection timed out after ${timeoutMs}ms`,
|
|
305
|
+
ERROR_CODES.CONNECTION_TIMEOUT,
|
|
306
|
+
{ retryable: true }
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Create a connection closed error
|
|
311
|
+
*/
|
|
312
|
+
static closed(reason) {
|
|
313
|
+
return new _ConnectionError(
|
|
314
|
+
reason ? `Connection closed: ${reason}` : "Connection closed unexpectedly",
|
|
315
|
+
ERROR_CODES.CONNECTION_CLOSED,
|
|
316
|
+
{ retryable: true }
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Create a connection failed error
|
|
321
|
+
*/
|
|
322
|
+
static failed(reason, cause) {
|
|
323
|
+
return new _ConnectionError(
|
|
324
|
+
reason ? `Connection failed: ${reason}` : "Failed to connect",
|
|
325
|
+
ERROR_CODES.CONNECTION_FAILED,
|
|
326
|
+
{ retryable: true, cause }
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
// src/client/ConnectionStateMachine.ts
|
|
332
|
+
var VALID_TRANSITIONS = [
|
|
333
|
+
{ from: "disconnected", to: "connecting" },
|
|
334
|
+
{ from: "connecting", to: "connected" },
|
|
335
|
+
{ from: "connecting", to: "disconnected" },
|
|
336
|
+
{ from: "connecting", to: "error" },
|
|
337
|
+
{ from: "connected", to: "disconnected" },
|
|
338
|
+
{ from: "connected", to: "error" },
|
|
339
|
+
{ from: "error", to: "connecting" },
|
|
340
|
+
{ from: "error", to: "disconnected" }
|
|
341
|
+
];
|
|
342
|
+
var ConnectionStateMachine = class {
|
|
343
|
+
_state = "disconnected";
|
|
344
|
+
listeners = /* @__PURE__ */ new Set();
|
|
345
|
+
/**
|
|
346
|
+
* Get current connection state
|
|
347
|
+
*/
|
|
348
|
+
get state() {
|
|
349
|
+
return this._state;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Check if currently in a specific state
|
|
353
|
+
*/
|
|
354
|
+
is(state) {
|
|
355
|
+
return this._state === state;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Check if connected
|
|
359
|
+
*/
|
|
360
|
+
get isConnected() {
|
|
361
|
+
return this._state === "connected";
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Check if connecting
|
|
365
|
+
*/
|
|
366
|
+
get isConnecting() {
|
|
367
|
+
return this._state === "connecting";
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Check if disconnected
|
|
371
|
+
*/
|
|
372
|
+
get isDisconnected() {
|
|
373
|
+
return this._state === "disconnected";
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Check if in error state
|
|
377
|
+
*/
|
|
378
|
+
get isError() {
|
|
379
|
+
return this._state === "error";
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Transition to a new state
|
|
383
|
+
* @returns true if transition was successful
|
|
384
|
+
*/
|
|
385
|
+
transition(to) {
|
|
386
|
+
if (this._state === to) {
|
|
387
|
+
return true;
|
|
388
|
+
}
|
|
389
|
+
const isValid = VALID_TRANSITIONS.some((t) => {
|
|
390
|
+
const fromStates = Array.isArray(t.from) ? t.from : [t.from];
|
|
391
|
+
return fromStates.includes(this._state) && t.to === to;
|
|
392
|
+
});
|
|
393
|
+
if (!isValid) {
|
|
394
|
+
console.warn(`Invalid state transition: ${this._state} -> ${to}`);
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
this._state = to;
|
|
398
|
+
this.listeners.forEach((listener) => {
|
|
399
|
+
try {
|
|
400
|
+
listener(to);
|
|
401
|
+
} catch (error) {
|
|
402
|
+
console.error("Error in connection state listener:", error);
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
return true;
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Force transition to a state (bypasses validation)
|
|
409
|
+
*/
|
|
410
|
+
forceTransition(to) {
|
|
411
|
+
if (this._state === to) {
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
this._state = to;
|
|
415
|
+
this.listeners.forEach((listener) => {
|
|
416
|
+
try {
|
|
417
|
+
listener(to);
|
|
418
|
+
} catch (error) {
|
|
419
|
+
console.error("Error in connection state listener:", error);
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Reset to disconnected state
|
|
425
|
+
*/
|
|
426
|
+
reset() {
|
|
427
|
+
this.forceTransition("disconnected");
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Subscribe to state changes
|
|
431
|
+
*/
|
|
432
|
+
onChange(listener) {
|
|
433
|
+
this.listeners.add(listener);
|
|
434
|
+
return () => this.listeners.delete(listener);
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Remove all listeners
|
|
438
|
+
*/
|
|
439
|
+
removeAllListeners() {
|
|
440
|
+
this.listeners.clear();
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
// src/client/Heartbeat.ts
|
|
445
|
+
var Heartbeat = class {
|
|
446
|
+
intervalId = null;
|
|
447
|
+
timeoutId = null;
|
|
448
|
+
interval;
|
|
449
|
+
pongTimeout;
|
|
450
|
+
onPing;
|
|
451
|
+
onTimeout;
|
|
452
|
+
running = false;
|
|
453
|
+
constructor(options) {
|
|
454
|
+
this.interval = options.interval ?? DEFAULTS.HEARTBEAT_INTERVAL;
|
|
455
|
+
this.pongTimeout = options.pongTimeout ?? this.interval * 2;
|
|
456
|
+
this.onPing = options.onPing;
|
|
457
|
+
this.onTimeout = options.onTimeout;
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Start the heartbeat
|
|
461
|
+
*/
|
|
462
|
+
start() {
|
|
463
|
+
if (this.running) {
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
this.running = true;
|
|
467
|
+
this.scheduleNextPing();
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Stop the heartbeat
|
|
471
|
+
*/
|
|
472
|
+
stop() {
|
|
473
|
+
this.running = false;
|
|
474
|
+
this.clearTimers();
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Called when a pong is received
|
|
478
|
+
*/
|
|
479
|
+
receivedPong() {
|
|
480
|
+
if (!this.running) {
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
if (this.timeoutId !== null) {
|
|
484
|
+
clearTimeout(this.timeoutId);
|
|
485
|
+
this.timeoutId = null;
|
|
486
|
+
}
|
|
487
|
+
this.scheduleNextPing();
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Reset the heartbeat (e.g., after any activity)
|
|
491
|
+
*/
|
|
492
|
+
reset() {
|
|
493
|
+
if (!this.running) {
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
this.clearTimers();
|
|
497
|
+
this.scheduleNextPing();
|
|
498
|
+
}
|
|
499
|
+
scheduleNextPing() {
|
|
500
|
+
if (!this.running) {
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
this.intervalId = setTimeout(() => {
|
|
504
|
+
if (!this.running) {
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
this.onPing();
|
|
508
|
+
this.timeoutId = setTimeout(() => {
|
|
509
|
+
if (this.running && this.onTimeout) {
|
|
510
|
+
this.onTimeout();
|
|
511
|
+
}
|
|
512
|
+
}, this.pongTimeout);
|
|
513
|
+
}, this.interval);
|
|
514
|
+
}
|
|
515
|
+
clearTimers() {
|
|
516
|
+
if (this.intervalId !== null) {
|
|
517
|
+
clearTimeout(this.intervalId);
|
|
518
|
+
this.intervalId = null;
|
|
519
|
+
}
|
|
520
|
+
if (this.timeoutId !== null) {
|
|
521
|
+
clearTimeout(this.timeoutId);
|
|
522
|
+
this.timeoutId = null;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
// src/client/WebSocketManager.ts
|
|
528
|
+
var WebSocketManager = class extends TypedEventEmitter {
|
|
529
|
+
options;
|
|
530
|
+
logger;
|
|
531
|
+
stateMachine;
|
|
532
|
+
heartbeat;
|
|
533
|
+
ws = null;
|
|
534
|
+
connectionTimeoutId = null;
|
|
535
|
+
reconnectAttempt = 0;
|
|
536
|
+
reconnectTimeoutId = null;
|
|
537
|
+
intentionalDisconnect = false;
|
|
538
|
+
clientId = null;
|
|
539
|
+
constructor(options) {
|
|
540
|
+
super();
|
|
541
|
+
this.options = {
|
|
542
|
+
apiKey: options.apiKey,
|
|
543
|
+
baseUrl: (options.baseUrl ?? DEFAULTS.BASE_URL).replace(/\/$/, ""),
|
|
544
|
+
connectionTimeout: options.connectionTimeout ?? DEFAULTS.CONNECTION_TIMEOUT,
|
|
545
|
+
heartbeatInterval: options.heartbeatInterval ?? DEFAULTS.HEARTBEAT_INTERVAL,
|
|
546
|
+
autoReconnect: options.autoReconnect ?? true,
|
|
547
|
+
maxReconnectAttempts: options.maxReconnectAttempts ?? DEFAULTS.MAX_RECONNECT_ATTEMPTS,
|
|
548
|
+
reconnectDelay: options.reconnectDelay ?? DEFAULTS.RECONNECT_DELAY,
|
|
549
|
+
maxReconnectDelay: options.maxReconnectDelay ?? DEFAULTS.MAX_RECONNECT_DELAY
|
|
550
|
+
};
|
|
551
|
+
this.logger = createLogger({ debug: options.debug, prefix: "[PushFlo WS]" });
|
|
552
|
+
this.stateMachine = new ConnectionStateMachine();
|
|
553
|
+
this.heartbeat = new Heartbeat({
|
|
554
|
+
interval: this.options.heartbeatInterval,
|
|
555
|
+
onPing: () => this.sendPing(),
|
|
556
|
+
onTimeout: () => this.handleHeartbeatTimeout()
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Get current connection state
|
|
561
|
+
*/
|
|
562
|
+
get state() {
|
|
563
|
+
return this.stateMachine.state;
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Get client ID (available after connected)
|
|
567
|
+
*/
|
|
568
|
+
getClientId() {
|
|
569
|
+
return this.clientId;
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Subscribe to state changes
|
|
573
|
+
*/
|
|
574
|
+
onStateChange(listener) {
|
|
575
|
+
return this.stateMachine.onChange(listener);
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Connect to WebSocket server
|
|
579
|
+
*/
|
|
580
|
+
async connect() {
|
|
581
|
+
if (this.stateMachine.isConnected) {
|
|
582
|
+
throw new ConnectionError(
|
|
583
|
+
"Already connected",
|
|
584
|
+
"ALREADY_CONNECTED",
|
|
585
|
+
{ retryable: false }
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
if (this.stateMachine.isConnecting) {
|
|
589
|
+
throw new ConnectionError(
|
|
590
|
+
"Connection in progress",
|
|
591
|
+
"CONNECTION_IN_PROGRESS",
|
|
592
|
+
{ retryable: false }
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
this.intentionalDisconnect = false;
|
|
596
|
+
this.clearReconnectTimeout();
|
|
597
|
+
return this.establishConnection();
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Disconnect from WebSocket server
|
|
601
|
+
*/
|
|
602
|
+
disconnect() {
|
|
603
|
+
this.intentionalDisconnect = true;
|
|
604
|
+
this.cleanup();
|
|
605
|
+
this.stateMachine.transition("disconnected");
|
|
606
|
+
this.emit("disconnected", "Disconnected by client");
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Send a message to the server
|
|
610
|
+
*/
|
|
611
|
+
send(message) {
|
|
612
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
613
|
+
return false;
|
|
614
|
+
}
|
|
615
|
+
try {
|
|
616
|
+
this.ws.send(JSON.stringify(message));
|
|
617
|
+
this.logger.debug("Sent message:", message);
|
|
618
|
+
return true;
|
|
619
|
+
} catch (error) {
|
|
620
|
+
this.logger.error("Failed to send message:", error);
|
|
621
|
+
return false;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Subscribe to a channel
|
|
626
|
+
*/
|
|
627
|
+
subscribe(channel) {
|
|
628
|
+
return this.send({
|
|
629
|
+
type: WS_CLIENT_MESSAGES.SUBSCRIBE,
|
|
630
|
+
channel
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Unsubscribe from a channel
|
|
635
|
+
*/
|
|
636
|
+
unsubscribe(channel) {
|
|
637
|
+
return this.send({
|
|
638
|
+
type: WS_CLIENT_MESSAGES.UNSUBSCRIBE,
|
|
639
|
+
channel
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Clean up resources
|
|
644
|
+
*/
|
|
645
|
+
destroy() {
|
|
646
|
+
this.intentionalDisconnect = true;
|
|
647
|
+
this.cleanup();
|
|
648
|
+
this.removeAllListeners();
|
|
649
|
+
this.stateMachine.removeAllListeners();
|
|
650
|
+
}
|
|
651
|
+
async establishConnection() {
|
|
652
|
+
this.stateMachine.transition("connecting");
|
|
653
|
+
this.logger.debug("Connecting...");
|
|
654
|
+
return new Promise((resolve, reject) => {
|
|
655
|
+
try {
|
|
656
|
+
const wsUrl = this.buildWsUrl();
|
|
657
|
+
this.logger.debug("WebSocket URL:", wsUrl);
|
|
658
|
+
this.ws = new WebSocket(wsUrl);
|
|
659
|
+
this.connectionTimeoutId = setTimeout(() => {
|
|
660
|
+
if (this.stateMachine.isConnecting) {
|
|
661
|
+
const error = ConnectionError.timeout(this.options.connectionTimeout);
|
|
662
|
+
this.cleanup();
|
|
663
|
+
this.stateMachine.transition("error");
|
|
664
|
+
this.emit("error", error);
|
|
665
|
+
reject(error);
|
|
666
|
+
}
|
|
667
|
+
}, this.options.connectionTimeout);
|
|
668
|
+
this.ws.onopen = () => {
|
|
669
|
+
this.logger.debug("WebSocket opened, waiting for connected message...");
|
|
670
|
+
};
|
|
671
|
+
this.ws.onclose = (event) => {
|
|
672
|
+
this.handleClose(event, reject);
|
|
673
|
+
};
|
|
674
|
+
this.ws.onerror = (event) => {
|
|
675
|
+
this.logger.error("WebSocket error:", event);
|
|
676
|
+
};
|
|
677
|
+
this.ws.onmessage = (event) => {
|
|
678
|
+
this.handleMessage(event, resolve, reject);
|
|
679
|
+
};
|
|
680
|
+
} catch (error) {
|
|
681
|
+
this.cleanup();
|
|
682
|
+
this.stateMachine.transition("error");
|
|
683
|
+
const connError = ConnectionError.failed(
|
|
684
|
+
error instanceof Error ? error.message : "Unknown error",
|
|
685
|
+
error instanceof Error ? error : void 0
|
|
686
|
+
);
|
|
687
|
+
this.emit("error", connError);
|
|
688
|
+
reject(connError);
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
buildWsUrl() {
|
|
693
|
+
const baseUrl = this.options.baseUrl;
|
|
694
|
+
const protocol = baseUrl.startsWith("https") ? "wss" : "ws";
|
|
695
|
+
const host = baseUrl.replace(/^https?:\/\//, "");
|
|
696
|
+
return `${protocol}://${host}${DEFAULTS.WS_PATH}?token=${encodeURIComponent(this.options.apiKey)}`;
|
|
697
|
+
}
|
|
698
|
+
handleMessage(event, onConnect, onConnectError) {
|
|
699
|
+
try {
|
|
700
|
+
const message = JSON.parse(event.data);
|
|
701
|
+
this.logger.debug("Received message:", message);
|
|
702
|
+
switch (message.type) {
|
|
703
|
+
case WS_SERVER_MESSAGES.CONNECTED:
|
|
704
|
+
this.handleConnected(message, onConnect);
|
|
705
|
+
break;
|
|
706
|
+
case WS_SERVER_MESSAGES.PONG:
|
|
707
|
+
this.heartbeat.receivedPong();
|
|
708
|
+
break;
|
|
709
|
+
case WS_SERVER_MESSAGES.ERROR:
|
|
710
|
+
this.handleErrorMessage(message, onConnectError);
|
|
711
|
+
break;
|
|
712
|
+
default:
|
|
713
|
+
this.emit("message", message);
|
|
714
|
+
}
|
|
715
|
+
} catch (error) {
|
|
716
|
+
this.logger.error("Failed to parse message:", error);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
handleConnected(message, onConnect) {
|
|
720
|
+
this.clearConnectionTimeout();
|
|
721
|
+
if (message.clientId) {
|
|
722
|
+
this.clientId = message.clientId;
|
|
723
|
+
}
|
|
724
|
+
const connectionInfo = {
|
|
725
|
+
clientId: message.clientId ?? "",
|
|
726
|
+
timestamp: message.timestamp ?? Date.now()
|
|
727
|
+
};
|
|
728
|
+
this.reconnectAttempt = 0;
|
|
729
|
+
this.stateMachine.transition("connected");
|
|
730
|
+
this.heartbeat.start();
|
|
731
|
+
this.logger.debug("Connected:", connectionInfo);
|
|
732
|
+
this.emit("connected", connectionInfo);
|
|
733
|
+
onConnect?.(connectionInfo);
|
|
734
|
+
}
|
|
735
|
+
handleErrorMessage(message, onConnectError) {
|
|
736
|
+
const errorMsg = message.error ?? "Unknown error";
|
|
737
|
+
const code = message.code;
|
|
738
|
+
let error;
|
|
739
|
+
if (code === "UNAUTHORIZED" || code === "INVALID_TOKEN") {
|
|
740
|
+
error = AuthenticationError.unauthorized(errorMsg);
|
|
741
|
+
} else {
|
|
742
|
+
error = new ConnectionError(errorMsg, code ?? "UNKNOWN");
|
|
743
|
+
}
|
|
744
|
+
this.emit("error", error);
|
|
745
|
+
if (this.stateMachine.isConnecting) {
|
|
746
|
+
this.cleanup();
|
|
747
|
+
this.stateMachine.transition("error");
|
|
748
|
+
onConnectError?.(error);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
handleClose(event, onConnectError) {
|
|
752
|
+
this.logger.debug("WebSocket closed:", event.code, event.reason);
|
|
753
|
+
const wasConnected = this.stateMachine.isConnected;
|
|
754
|
+
this.cleanup();
|
|
755
|
+
if (this.intentionalDisconnect) {
|
|
756
|
+
this.stateMachine.transition("disconnected");
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
if (this.stateMachine.isConnecting) {
|
|
760
|
+
this.stateMachine.transition("error");
|
|
761
|
+
const error = ConnectionError.failed(event.reason || "Connection closed");
|
|
762
|
+
this.emit("error", error);
|
|
763
|
+
onConnectError?.(error);
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
this.stateMachine.transition("disconnected");
|
|
767
|
+
this.emit("disconnected", event.reason || void 0);
|
|
768
|
+
if (wasConnected && this.options.autoReconnect) {
|
|
769
|
+
this.scheduleReconnect();
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
handleHeartbeatTimeout() {
|
|
773
|
+
this.logger.warn("Heartbeat timeout, reconnecting...");
|
|
774
|
+
this.cleanup();
|
|
775
|
+
this.stateMachine.transition("disconnected");
|
|
776
|
+
this.emit("disconnected", "Heartbeat timeout");
|
|
777
|
+
if (this.options.autoReconnect) {
|
|
778
|
+
this.scheduleReconnect();
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
scheduleReconnect() {
|
|
782
|
+
if (this.intentionalDisconnect) {
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
const { maxReconnectAttempts } = this.options;
|
|
786
|
+
if (maxReconnectAttempts > 0 && this.reconnectAttempt >= maxReconnectAttempts) {
|
|
787
|
+
this.logger.error("Max reconnect attempts reached");
|
|
788
|
+
this.stateMachine.transition("error");
|
|
789
|
+
this.emit("error", new ConnectionError(
|
|
790
|
+
"Max reconnection attempts exceeded",
|
|
791
|
+
"MAX_RECONNECT_ATTEMPTS",
|
|
792
|
+
{ retryable: false }
|
|
793
|
+
));
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
const delay = calculateBackoff(this.reconnectAttempt, {
|
|
797
|
+
initialDelay: this.options.reconnectDelay,
|
|
798
|
+
maxDelay: this.options.maxReconnectDelay
|
|
799
|
+
});
|
|
800
|
+
this.logger.debug(`Scheduling reconnect in ${delay}ms (attempt ${this.reconnectAttempt + 1})`);
|
|
801
|
+
this.reconnectTimeoutId = setTimeout(() => {
|
|
802
|
+
this.reconnectAttempt++;
|
|
803
|
+
this.establishConnection().catch((error) => {
|
|
804
|
+
this.logger.error("Reconnect failed:", error);
|
|
805
|
+
this.scheduleReconnect();
|
|
806
|
+
});
|
|
807
|
+
}, delay);
|
|
808
|
+
}
|
|
809
|
+
sendPing() {
|
|
810
|
+
this.send({ type: WS_CLIENT_MESSAGES.PING });
|
|
811
|
+
}
|
|
812
|
+
cleanup() {
|
|
813
|
+
this.clearConnectionTimeout();
|
|
814
|
+
this.heartbeat.stop();
|
|
815
|
+
if (this.ws) {
|
|
816
|
+
this.ws.onopen = null;
|
|
817
|
+
this.ws.onclose = null;
|
|
818
|
+
this.ws.onerror = null;
|
|
819
|
+
this.ws.onmessage = null;
|
|
820
|
+
if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) {
|
|
821
|
+
this.ws.close();
|
|
822
|
+
}
|
|
823
|
+
this.ws = null;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
clearConnectionTimeout() {
|
|
827
|
+
if (this.connectionTimeoutId !== null) {
|
|
828
|
+
clearTimeout(this.connectionTimeoutId);
|
|
829
|
+
this.connectionTimeoutId = null;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
clearReconnectTimeout() {
|
|
833
|
+
if (this.reconnectTimeoutId !== null) {
|
|
834
|
+
clearTimeout(this.reconnectTimeoutId);
|
|
835
|
+
this.reconnectTimeoutId = null;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
};
|
|
839
|
+
|
|
840
|
+
// src/client/SubscriptionManager.ts
|
|
841
|
+
var SubscriptionManager = class {
|
|
842
|
+
subscriptions = /* @__PURE__ */ new Map();
|
|
843
|
+
/**
|
|
844
|
+
* Add a subscription
|
|
845
|
+
*/
|
|
846
|
+
add(channel, options = {}) {
|
|
847
|
+
this.subscriptions.set(channel, {
|
|
848
|
+
channel,
|
|
849
|
+
options,
|
|
850
|
+
confirmed: false
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* Remove a subscription
|
|
855
|
+
*/
|
|
856
|
+
remove(channel) {
|
|
857
|
+
const entry = this.subscriptions.get(channel);
|
|
858
|
+
if (entry) {
|
|
859
|
+
this.subscriptions.delete(channel);
|
|
860
|
+
entry.options.onUnsubscribed?.();
|
|
861
|
+
return true;
|
|
862
|
+
}
|
|
863
|
+
return false;
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* Check if subscribed to a channel
|
|
867
|
+
*/
|
|
868
|
+
has(channel) {
|
|
869
|
+
return this.subscriptions.has(channel);
|
|
870
|
+
}
|
|
871
|
+
/**
|
|
872
|
+
* Get subscription options for a channel
|
|
873
|
+
*/
|
|
874
|
+
get(channel) {
|
|
875
|
+
return this.subscriptions.get(channel)?.options;
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* Mark subscription as confirmed
|
|
879
|
+
*/
|
|
880
|
+
confirm(channel) {
|
|
881
|
+
const entry = this.subscriptions.get(channel);
|
|
882
|
+
if (entry) {
|
|
883
|
+
entry.confirmed = true;
|
|
884
|
+
entry.options.onSubscribed?.();
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
/**
|
|
888
|
+
* Check if subscription is confirmed
|
|
889
|
+
*/
|
|
890
|
+
isConfirmed(channel) {
|
|
891
|
+
return this.subscriptions.get(channel)?.confirmed ?? false;
|
|
892
|
+
}
|
|
893
|
+
/**
|
|
894
|
+
* Handle incoming message
|
|
895
|
+
*/
|
|
896
|
+
handleMessage(message) {
|
|
897
|
+
const entry = this.subscriptions.get(message.channel);
|
|
898
|
+
if (entry) {
|
|
899
|
+
entry.options.onMessage?.(message);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* Handle subscription error
|
|
904
|
+
*/
|
|
905
|
+
handleError(channel, error) {
|
|
906
|
+
const entry = this.subscriptions.get(channel);
|
|
907
|
+
if (entry) {
|
|
908
|
+
entry.options.onError?.(error);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* Get all subscribed channel names
|
|
913
|
+
*/
|
|
914
|
+
getChannels() {
|
|
915
|
+
return Array.from(this.subscriptions.keys());
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* Get count of subscriptions
|
|
919
|
+
*/
|
|
920
|
+
get size() {
|
|
921
|
+
return this.subscriptions.size;
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* Clear all subscriptions
|
|
925
|
+
*/
|
|
926
|
+
clear() {
|
|
927
|
+
this.subscriptions.forEach((entry) => {
|
|
928
|
+
entry.options.onUnsubscribed?.();
|
|
929
|
+
});
|
|
930
|
+
this.subscriptions.clear();
|
|
931
|
+
}
|
|
932
|
+
/**
|
|
933
|
+
* Reset confirmation status for all subscriptions (e.g., on reconnect)
|
|
934
|
+
*/
|
|
935
|
+
resetConfirmations() {
|
|
936
|
+
this.subscriptions.forEach((entry) => {
|
|
937
|
+
entry.confirmed = false;
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
};
|
|
941
|
+
|
|
942
|
+
// src/client/PushFloClient.ts
|
|
943
|
+
var PushFloClient = class extends TypedEventEmitter {
|
|
944
|
+
wsManager;
|
|
945
|
+
subscriptions;
|
|
946
|
+
logger;
|
|
947
|
+
connectionChangeListeners = /* @__PURE__ */ new Set();
|
|
948
|
+
constructor(options) {
|
|
949
|
+
super();
|
|
950
|
+
if (!options.publishKey) {
|
|
951
|
+
throw new AuthenticationError(
|
|
952
|
+
"Publish key is required",
|
|
953
|
+
"MISSING_PUBLISH_KEY"
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
if (!options.publishKey.startsWith("pub_") && !options.publishKey.startsWith("sec_") && !options.publishKey.startsWith("mgmt_")) {
|
|
957
|
+
throw AuthenticationError.invalidKey("publish");
|
|
958
|
+
}
|
|
959
|
+
this.logger = createLogger({ debug: options.debug, prefix: "[PushFlo]" });
|
|
960
|
+
this.subscriptions = new SubscriptionManager();
|
|
961
|
+
this.wsManager = new WebSocketManager({
|
|
962
|
+
apiKey: options.publishKey,
|
|
963
|
+
baseUrl: options.baseUrl ?? DEFAULTS.BASE_URL,
|
|
964
|
+
connectionTimeout: options.connectionTimeout,
|
|
965
|
+
heartbeatInterval: options.heartbeatInterval,
|
|
966
|
+
autoReconnect: options.autoReconnect,
|
|
967
|
+
maxReconnectAttempts: options.maxReconnectAttempts,
|
|
968
|
+
reconnectDelay: options.reconnectDelay,
|
|
969
|
+
maxReconnectDelay: options.maxReconnectDelay,
|
|
970
|
+
debug: options.debug
|
|
971
|
+
});
|
|
972
|
+
this.setupEventHandlers();
|
|
973
|
+
if (options.autoConnect) {
|
|
974
|
+
this.connect().catch((error) => {
|
|
975
|
+
this.logger.error("Auto-connect failed:", error);
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
/**
|
|
980
|
+
* Get current connection state
|
|
981
|
+
*/
|
|
982
|
+
get connectionState() {
|
|
983
|
+
return this.wsManager.state;
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Get client ID (available after connected)
|
|
987
|
+
*/
|
|
988
|
+
get clientId() {
|
|
989
|
+
return this.wsManager.getClientId();
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* Connect to PushFlo
|
|
993
|
+
*/
|
|
994
|
+
async connect() {
|
|
995
|
+
this.logger.debug("Connecting...");
|
|
996
|
+
await this.wsManager.connect();
|
|
997
|
+
const channels = this.subscriptions.getChannels();
|
|
998
|
+
if (channels.length > 0) {
|
|
999
|
+
this.logger.debug("Re-subscribing to channels:", channels);
|
|
1000
|
+
this.subscriptions.resetConfirmations();
|
|
1001
|
+
channels.forEach((channel) => {
|
|
1002
|
+
this.wsManager.subscribe(channel);
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Disconnect from PushFlo
|
|
1008
|
+
*/
|
|
1009
|
+
disconnect() {
|
|
1010
|
+
this.logger.debug("Disconnecting...");
|
|
1011
|
+
this.wsManager.disconnect();
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Clean up all resources
|
|
1015
|
+
*/
|
|
1016
|
+
destroy() {
|
|
1017
|
+
this.logger.debug("Destroying client...");
|
|
1018
|
+
this.subscriptions.clear();
|
|
1019
|
+
this.wsManager.destroy();
|
|
1020
|
+
this.connectionChangeListeners.clear();
|
|
1021
|
+
this.removeAllListeners();
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* Subscribe to a channel
|
|
1025
|
+
*/
|
|
1026
|
+
subscribe(channel, options = {}) {
|
|
1027
|
+
if (!channel) {
|
|
1028
|
+
throw new PushFloError("Channel is required", "INVALID_CHANNEL", { retryable: false });
|
|
1029
|
+
}
|
|
1030
|
+
this.logger.debug("Subscribing to channel:", channel);
|
|
1031
|
+
this.subscriptions.add(channel, options);
|
|
1032
|
+
if (this.wsManager.state === "connected") {
|
|
1033
|
+
this.wsManager.subscribe(channel);
|
|
1034
|
+
}
|
|
1035
|
+
return {
|
|
1036
|
+
channel,
|
|
1037
|
+
unsubscribe: () => this.unsubscribe(channel)
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
/**
|
|
1041
|
+
* Unsubscribe from a channel
|
|
1042
|
+
*/
|
|
1043
|
+
unsubscribe(channel) {
|
|
1044
|
+
this.logger.debug("Unsubscribing from channel:", channel);
|
|
1045
|
+
this.subscriptions.remove(channel);
|
|
1046
|
+
if (this.wsManager.state === "connected") {
|
|
1047
|
+
this.wsManager.unsubscribe(channel);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
/**
|
|
1051
|
+
* Register a connection state change listener
|
|
1052
|
+
*/
|
|
1053
|
+
onConnectionChange(listener) {
|
|
1054
|
+
this.connectionChangeListeners.add(listener);
|
|
1055
|
+
return () => this.connectionChangeListeners.delete(listener);
|
|
1056
|
+
}
|
|
1057
|
+
/**
|
|
1058
|
+
* Get list of subscribed channels
|
|
1059
|
+
*/
|
|
1060
|
+
getSubscribedChannels() {
|
|
1061
|
+
return this.subscriptions.getChannels();
|
|
1062
|
+
}
|
|
1063
|
+
/**
|
|
1064
|
+
* Check if subscribed to a channel
|
|
1065
|
+
*/
|
|
1066
|
+
isSubscribed(channel) {
|
|
1067
|
+
return this.subscriptions.has(channel);
|
|
1068
|
+
}
|
|
1069
|
+
setupEventHandlers() {
|
|
1070
|
+
this.wsManager.onStateChange((state) => {
|
|
1071
|
+
this.connectionChangeListeners.forEach((listener) => {
|
|
1072
|
+
try {
|
|
1073
|
+
listener(state);
|
|
1074
|
+
} catch (error) {
|
|
1075
|
+
this.logger.error("Error in connection change listener:", error);
|
|
1076
|
+
}
|
|
1077
|
+
});
|
|
1078
|
+
});
|
|
1079
|
+
this.wsManager.on("connected", (info) => {
|
|
1080
|
+
this.emit("connected", info);
|
|
1081
|
+
});
|
|
1082
|
+
this.wsManager.on("disconnected", (reason) => {
|
|
1083
|
+
this.emit("disconnected", reason);
|
|
1084
|
+
});
|
|
1085
|
+
this.wsManager.on("error", (error) => {
|
|
1086
|
+
this.emit("error", error);
|
|
1087
|
+
});
|
|
1088
|
+
this.wsManager.on("message", (message) => {
|
|
1089
|
+
this.handleServerMessage(message);
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
1092
|
+
handleServerMessage(message) {
|
|
1093
|
+
switch (message.type) {
|
|
1094
|
+
case WS_SERVER_MESSAGES.SUBSCRIBED:
|
|
1095
|
+
if (message.channel) {
|
|
1096
|
+
this.subscriptions.confirm(message.channel);
|
|
1097
|
+
this.logger.debug("Subscribed to channel:", message.channel);
|
|
1098
|
+
}
|
|
1099
|
+
break;
|
|
1100
|
+
case WS_SERVER_MESSAGES.UNSUBSCRIBED:
|
|
1101
|
+
if (message.channel) {
|
|
1102
|
+
this.logger.debug("Unsubscribed from channel:", message.channel);
|
|
1103
|
+
}
|
|
1104
|
+
break;
|
|
1105
|
+
case WS_SERVER_MESSAGES.MESSAGE:
|
|
1106
|
+
if (message.channel && message.message) {
|
|
1107
|
+
const fullMessage = {
|
|
1108
|
+
id: message.message.id,
|
|
1109
|
+
channel: message.channel,
|
|
1110
|
+
eventType: message.message.eventType,
|
|
1111
|
+
clientId: message.message.clientId,
|
|
1112
|
+
content: message.message.content,
|
|
1113
|
+
timestamp: message.message.timestamp
|
|
1114
|
+
};
|
|
1115
|
+
this.subscriptions.handleMessage(fullMessage);
|
|
1116
|
+
this.emit("message", fullMessage);
|
|
1117
|
+
}
|
|
1118
|
+
break;
|
|
1119
|
+
case WS_SERVER_MESSAGES.ERROR:
|
|
1120
|
+
if (message.channel) {
|
|
1121
|
+
const error = new PushFloError(
|
|
1122
|
+
message.error ?? "Unknown error",
|
|
1123
|
+
message.code ?? ERROR_CODES.SERVER_ERROR
|
|
1124
|
+
);
|
|
1125
|
+
this.subscriptions.handleError(message.channel, error);
|
|
1126
|
+
}
|
|
1127
|
+
break;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
};
|
|
1131
|
+
|
|
1132
|
+
// src/errors/NetworkError.ts
|
|
1133
|
+
var NetworkError = class _NetworkError extends PushFloError {
|
|
1134
|
+
/** HTTP status code, if applicable */
|
|
1135
|
+
statusCode;
|
|
1136
|
+
constructor(message, code = ERROR_CODES.NETWORK_ERROR, options = {}) {
|
|
1137
|
+
super(message, code, { retryable: true, ...options });
|
|
1138
|
+
this.name = "NetworkError";
|
|
1139
|
+
this.statusCode = options.statusCode;
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* Create a network error from fetch failure
|
|
1143
|
+
*/
|
|
1144
|
+
static fromFetch(cause) {
|
|
1145
|
+
return new _NetworkError(
|
|
1146
|
+
`Network request failed: ${cause.message}`,
|
|
1147
|
+
ERROR_CODES.NETWORK_ERROR,
|
|
1148
|
+
{ retryable: true, cause }
|
|
1149
|
+
);
|
|
1150
|
+
}
|
|
1151
|
+
/**
|
|
1152
|
+
* Create a request timeout error
|
|
1153
|
+
*/
|
|
1154
|
+
static timeout(timeoutMs) {
|
|
1155
|
+
return new _NetworkError(
|
|
1156
|
+
`Request timed out after ${timeoutMs}ms`,
|
|
1157
|
+
ERROR_CODES.REQUEST_TIMEOUT,
|
|
1158
|
+
{ retryable: true }
|
|
1159
|
+
);
|
|
1160
|
+
}
|
|
1161
|
+
/**
|
|
1162
|
+
* Create an error from HTTP status code
|
|
1163
|
+
*/
|
|
1164
|
+
static fromStatus(statusCode, message) {
|
|
1165
|
+
const defaultMessage = _NetworkError.getStatusMessage(statusCode);
|
|
1166
|
+
const retryable = statusCode >= 500 || statusCode === 429;
|
|
1167
|
+
let code = ERROR_CODES.SERVER_ERROR;
|
|
1168
|
+
if (statusCode === 404) {
|
|
1169
|
+
code = ERROR_CODES.NOT_FOUND;
|
|
1170
|
+
} else if (statusCode === 422 || statusCode === 400) {
|
|
1171
|
+
code = ERROR_CODES.VALIDATION_ERROR;
|
|
1172
|
+
} else if (statusCode === 429) {
|
|
1173
|
+
code = ERROR_CODES.RATE_LIMITED;
|
|
1174
|
+
}
|
|
1175
|
+
return new _NetworkError(
|
|
1176
|
+
message ?? defaultMessage,
|
|
1177
|
+
code,
|
|
1178
|
+
{ retryable, statusCode }
|
|
1179
|
+
);
|
|
1180
|
+
}
|
|
1181
|
+
static getStatusMessage(statusCode) {
|
|
1182
|
+
const messages = {
|
|
1183
|
+
400: "Bad request",
|
|
1184
|
+
404: "Resource not found",
|
|
1185
|
+
422: "Validation error",
|
|
1186
|
+
429: "Rate limit exceeded",
|
|
1187
|
+
500: "Internal server error",
|
|
1188
|
+
502: "Bad gateway",
|
|
1189
|
+
503: "Service unavailable",
|
|
1190
|
+
504: "Gateway timeout"
|
|
1191
|
+
};
|
|
1192
|
+
return messages[statusCode] ?? `HTTP error ${statusCode}`;
|
|
1193
|
+
}
|
|
1194
|
+
toJSON() {
|
|
1195
|
+
return {
|
|
1196
|
+
...super.toJSON(),
|
|
1197
|
+
statusCode: this.statusCode
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
};
|
|
1201
|
+
|
|
1202
|
+
export { AuthenticationError, ConnectionError, NetworkError, PushFloClient, PushFloError };
|
|
1203
|
+
//# sourceMappingURL=index.js.map
|
|
1204
|
+
//# sourceMappingURL=index.js.map
|