pushstream-client 0.1.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/LICENSE +21 -0
- package/README.md +294 -0
- package/dist/pushstream.cjs.js +534 -0
- package/dist/pushstream.cjs.js.map +1 -0
- package/dist/pushstream.esm.js +528 -0
- package/dist/pushstream.esm.js.map +1 -0
- package/dist/pushstream.js +540 -0
- package/dist/pushstream.js.map +1 -0
- package/dist/pushstream.min.js +8 -0
- package/dist/pushstream.min.js.map +1 -0
- package/package.json +57 -0
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* PushStream JavaScript Client v0.1.0
|
|
3
|
+
* A lightweight, zero-dependency SSE client library
|
|
4
|
+
* (c) 2026
|
|
5
|
+
* Released under the MIT License
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Connection states for EventClient
|
|
9
|
+
* @readonly
|
|
10
|
+
* @enum {string}
|
|
11
|
+
*/
|
|
12
|
+
const ConnectionState = Object.freeze({
|
|
13
|
+
/** Client is not connected */
|
|
14
|
+
DISCONNECTED: 'disconnected',
|
|
15
|
+
/** Client is attempting to connect */
|
|
16
|
+
CONNECTING: 'connecting',
|
|
17
|
+
/** Client is connected and receiving events */
|
|
18
|
+
CONNECTED: 'connected'
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Built-in event names emitted by EventClient
|
|
23
|
+
* @readonly
|
|
24
|
+
* @enum {string}
|
|
25
|
+
*/
|
|
26
|
+
const BuiltInEvents = Object.freeze({
|
|
27
|
+
/** Emitted when connection is established */
|
|
28
|
+
OPEN: 'stream.open',
|
|
29
|
+
/** Emitted when connection is closed */
|
|
30
|
+
CLOSE: 'stream.close',
|
|
31
|
+
/** Emitted when an error occurs */
|
|
32
|
+
ERROR: 'stream.error',
|
|
33
|
+
/** Emitted when connection state changes */
|
|
34
|
+
STATE_CHANGE: 'stream.statechange'
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Default options for EventClient
|
|
39
|
+
* @readonly
|
|
40
|
+
*/
|
|
41
|
+
const DefaultOptions = Object.freeze({
|
|
42
|
+
/** Whether to automatically reconnect on connection loss */
|
|
43
|
+
reconnect: true,
|
|
44
|
+
/** Base delay in milliseconds between reconnection attempts */
|
|
45
|
+
reconnectInterval: 1000,
|
|
46
|
+
/** Maximum number of reconnection attempts before giving up */
|
|
47
|
+
maxReconnectAttempts: 10,
|
|
48
|
+
/** Maximum delay cap for exponential backoff (30 seconds) */
|
|
49
|
+
maxReconnectDelay: 30000,
|
|
50
|
+
/** Whether to include credentials in cross-origin requests */
|
|
51
|
+
withCredentials: false
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Manages event subscriptions with O(1) add/remove operations.
|
|
56
|
+
* Uses Map for event->callbacks storage and Set for callback deduplication.
|
|
57
|
+
*/
|
|
58
|
+
class SubscriptionManager {
|
|
59
|
+
constructor() {
|
|
60
|
+
/** @type {Map<string, Set<Function>>} */
|
|
61
|
+
this._listeners = new Map();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Register a callback for a specific event.
|
|
66
|
+
* @param {string} event - The event name to subscribe to
|
|
67
|
+
* @param {Function} callback - The callback to invoke when the event occurs
|
|
68
|
+
* @returns {boolean} True if the callback was added, false if already exists
|
|
69
|
+
*/
|
|
70
|
+
add(event, callback) {
|
|
71
|
+
if (typeof callback !== 'function') {
|
|
72
|
+
throw new TypeError('Callback must be a function');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!this._listeners.has(event)) {
|
|
76
|
+
this._listeners.set(event, new Set());
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const callbacks = this._listeners.get(event);
|
|
80
|
+
const existed = callbacks.has(callback);
|
|
81
|
+
callbacks.add(callback);
|
|
82
|
+
|
|
83
|
+
return !existed;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Remove a specific callback for an event.
|
|
88
|
+
* @param {string} event - The event name
|
|
89
|
+
* @param {Function} callback - The callback to remove
|
|
90
|
+
* @returns {boolean} True if the callback was removed
|
|
91
|
+
*/
|
|
92
|
+
remove(event, callback) {
|
|
93
|
+
const callbacks = this._listeners.get(event);
|
|
94
|
+
if (!callbacks) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const removed = callbacks.delete(callback);
|
|
99
|
+
|
|
100
|
+
// Clean up empty sets
|
|
101
|
+
if (callbacks.size === 0) {
|
|
102
|
+
this._listeners.delete(event);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return removed;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Remove all callbacks for a specific event.
|
|
110
|
+
* @param {string} event - The event name
|
|
111
|
+
* @returns {boolean} True if any callbacks were removed
|
|
112
|
+
*/
|
|
113
|
+
removeAll(event) {
|
|
114
|
+
return this._listeners.delete(event);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Emit an event to all registered callbacks.
|
|
119
|
+
* Creates a snapshot of callbacks to allow safe modification during iteration.
|
|
120
|
+
* @param {string} event - The event name
|
|
121
|
+
* @param {*} data - The data to pass to callbacks
|
|
122
|
+
*/
|
|
123
|
+
emit(event, data) {
|
|
124
|
+
const callbacks = this._listeners.get(event);
|
|
125
|
+
if (!callbacks || callbacks.size === 0) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Create snapshot to allow modifications during iteration
|
|
130
|
+
const snapshot = Array.from(callbacks);
|
|
131
|
+
|
|
132
|
+
for (const callback of snapshot) {
|
|
133
|
+
try {
|
|
134
|
+
callback(data);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
// Log but don't throw to prevent one bad callback from breaking others
|
|
137
|
+
console.error(`Error in event callback for "${event}":`, error);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Check if an event has any subscribers.
|
|
144
|
+
* @param {string} event - The event name
|
|
145
|
+
* @returns {boolean} True if the event has subscribers
|
|
146
|
+
*/
|
|
147
|
+
has(event) {
|
|
148
|
+
const callbacks = this._listeners.get(event);
|
|
149
|
+
return callbacks !== undefined && callbacks.size > 0;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Get all registered event names.
|
|
154
|
+
* @returns {string[]} Array of event names
|
|
155
|
+
*/
|
|
156
|
+
getEvents() {
|
|
157
|
+
return Array.from(this._listeners.keys());
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Get the number of callbacks for a specific event.
|
|
162
|
+
* @param {string} event - The event name
|
|
163
|
+
* @returns {number} Number of callbacks
|
|
164
|
+
*/
|
|
165
|
+
getCount(event) {
|
|
166
|
+
const callbacks = this._listeners.get(event);
|
|
167
|
+
return callbacks ? callbacks.size : 0;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Clear all subscriptions.
|
|
172
|
+
*/
|
|
173
|
+
clear() {
|
|
174
|
+
this._listeners.clear();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* EventClient provides a clean abstraction over EventSource for consuming SSE events.
|
|
180
|
+
*
|
|
181
|
+
* Features:
|
|
182
|
+
* - Automatic reconnection with exponential backoff and jitter
|
|
183
|
+
* - Event subscription management
|
|
184
|
+
* - Automatic JSON payload parsing
|
|
185
|
+
* - Connection state tracking
|
|
186
|
+
* - Built-in lifecycle events
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* const client = new EventClient('/events');
|
|
190
|
+
* client.on('task.progress', (data) => console.log(data.percentage));
|
|
191
|
+
* client.connect();
|
|
192
|
+
*/
|
|
193
|
+
class EventClient {
|
|
194
|
+
/**
|
|
195
|
+
* Create a new EventClient instance.
|
|
196
|
+
* @param {string} url - The SSE endpoint URL (relative or absolute)
|
|
197
|
+
* @param {Object} [options] - Configuration options
|
|
198
|
+
* @param {boolean} [options.reconnect=true] - Enable automatic reconnection
|
|
199
|
+
* @param {number} [options.reconnectInterval=1000] - Base reconnection delay in ms
|
|
200
|
+
* @param {number} [options.maxReconnectAttempts=10] - Maximum reconnection attempts
|
|
201
|
+
* @param {number} [options.maxReconnectDelay=30000] - Maximum delay cap in ms
|
|
202
|
+
* @param {boolean} [options.withCredentials=false] - Include credentials in CORS requests
|
|
203
|
+
*/
|
|
204
|
+
constructor(url, options = {}) {
|
|
205
|
+
if (!url || typeof url !== 'string') {
|
|
206
|
+
throw new TypeError('URL must be a non-empty string');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
this._url = url;
|
|
210
|
+
this._options = { ...DefaultOptions, ...options };
|
|
211
|
+
this._eventSource = null;
|
|
212
|
+
this._subscriptions = new SubscriptionManager();
|
|
213
|
+
this._state = ConnectionState.DISCONNECTED;
|
|
214
|
+
this._reconnectAttempts = 0;
|
|
215
|
+
this._reconnectTimer = null;
|
|
216
|
+
this._manualDisconnect = false;
|
|
217
|
+
this._registeredEventTypes = new Set();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Get the current connection state.
|
|
222
|
+
* @returns {string} One of: 'disconnected', 'connecting', 'connected'
|
|
223
|
+
*/
|
|
224
|
+
get state() {
|
|
225
|
+
return this._state;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Get the endpoint URL.
|
|
230
|
+
* @returns {string} The SSE endpoint URL
|
|
231
|
+
*/
|
|
232
|
+
get url() {
|
|
233
|
+
return this._url;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Establish an SSE connection to the server.
|
|
238
|
+
* This method is idempotent - calling it while already connected has no effect.
|
|
239
|
+
*/
|
|
240
|
+
connect() {
|
|
241
|
+
// Idempotent: don't reconnect if already connecting or connected
|
|
242
|
+
if (this._state !== ConnectionState.DISCONNECTED) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
this._manualDisconnect = false;
|
|
247
|
+
this._setState(ConnectionState.CONNECTING);
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
this._eventSource = new EventSource(this._url, {
|
|
251
|
+
withCredentials: this._options.withCredentials
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
this._eventSource.onopen = () => this._handleOpen();
|
|
255
|
+
this._eventSource.onerror = (event) => this._handleError(event);
|
|
256
|
+
|
|
257
|
+
// Register event listeners for all currently subscribed event types
|
|
258
|
+
this._registerEventListeners();
|
|
259
|
+
} catch (error) {
|
|
260
|
+
this._handleConnectionError(error);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Close the SSE connection.
|
|
266
|
+
* This method is idempotent - calling it while already disconnected has no effect.
|
|
267
|
+
* After calling disconnect(), no automatic reconnection will be attempted.
|
|
268
|
+
*/
|
|
269
|
+
disconnect() {
|
|
270
|
+
this._manualDisconnect = true;
|
|
271
|
+
this._cleanup();
|
|
272
|
+
|
|
273
|
+
if (this._state !== ConnectionState.DISCONNECTED) {
|
|
274
|
+
this._setState(ConnectionState.DISCONNECTED);
|
|
275
|
+
this._emit(BuiltInEvents.CLOSE, { manual: true });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Subscribe to an event.
|
|
281
|
+
* Subscriptions can be registered before or after connecting.
|
|
282
|
+
* @param {string} event - The event name to subscribe to
|
|
283
|
+
* @param {Function} callback - The callback to invoke when the event occurs
|
|
284
|
+
* @returns {EventClient} This instance for chaining
|
|
285
|
+
*/
|
|
286
|
+
on(event, callback) {
|
|
287
|
+
if (typeof event !== 'string' || !event) {
|
|
288
|
+
throw new TypeError('Event name must be a non-empty string');
|
|
289
|
+
}
|
|
290
|
+
if (typeof callback !== 'function') {
|
|
291
|
+
throw new TypeError('Callback must be a function');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
this._subscriptions.add(event, callback);
|
|
295
|
+
|
|
296
|
+
// If already connected and this is a new event type, register it
|
|
297
|
+
if (this._eventSource && !this._isBuiltInEvent(event) && !this._registeredEventTypes.has(event)) {
|
|
298
|
+
this._registerSingleEventListener(event);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return this;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Unsubscribe from an event.
|
|
306
|
+
* @param {string} event - The event name
|
|
307
|
+
* @param {Function} [callback] - Specific callback to remove. If omitted, removes all callbacks for the event.
|
|
308
|
+
* @returns {EventClient} This instance for chaining
|
|
309
|
+
*/
|
|
310
|
+
off(event, callback) {
|
|
311
|
+
if (typeof event !== 'string' || !event) {
|
|
312
|
+
throw new TypeError('Event name must be a non-empty string');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (callback !== undefined) {
|
|
316
|
+
this._subscriptions.remove(event, callback);
|
|
317
|
+
} else {
|
|
318
|
+
this._subscriptions.removeAll(event);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return this;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// =====================
|
|
325
|
+
// Private Methods
|
|
326
|
+
// =====================
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Update connection state and emit state change event.
|
|
330
|
+
* @private
|
|
331
|
+
*/
|
|
332
|
+
_setState(newState) {
|
|
333
|
+
const oldState = this._state;
|
|
334
|
+
if (oldState === newState) {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
this._state = newState;
|
|
339
|
+
this._emit(BuiltInEvents.STATE_CHANGE, {
|
|
340
|
+
previousState: oldState,
|
|
341
|
+
currentState: newState
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Emit an event to all subscribers.
|
|
347
|
+
* @private
|
|
348
|
+
*/
|
|
349
|
+
_emit(event, data) {
|
|
350
|
+
this._subscriptions.emit(event, data);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Handle successful connection.
|
|
355
|
+
* @private
|
|
356
|
+
*/
|
|
357
|
+
_handleOpen() {
|
|
358
|
+
this._reconnectAttempts = 0; // Reset on successful connection
|
|
359
|
+
this._setState(ConnectionState.CONNECTED);
|
|
360
|
+
this._emit(BuiltInEvents.OPEN, { url: this._url });
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Handle connection error.
|
|
365
|
+
* @private
|
|
366
|
+
*/
|
|
367
|
+
_handleError(event) {
|
|
368
|
+
// EventSource error event doesn't provide much detail
|
|
369
|
+
const errorInfo = {
|
|
370
|
+
message: 'Connection error',
|
|
371
|
+
readyState: this._eventSource?.readyState
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
this._emit(BuiltInEvents.ERROR, errorInfo);
|
|
375
|
+
|
|
376
|
+
// Check if connection was lost
|
|
377
|
+
if (this._eventSource?.readyState === EventSource.CLOSED) {
|
|
378
|
+
this._handleConnectionLoss();
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Handle initial connection failure.
|
|
384
|
+
* @private
|
|
385
|
+
*/
|
|
386
|
+
_handleConnectionError(error) {
|
|
387
|
+
this._setState(ConnectionState.DISCONNECTED);
|
|
388
|
+
this._emit(BuiltInEvents.ERROR, {
|
|
389
|
+
message: error.message || 'Failed to create connection',
|
|
390
|
+
error
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Handle connection loss and schedule reconnection.
|
|
396
|
+
* @private
|
|
397
|
+
*/
|
|
398
|
+
_handleConnectionLoss() {
|
|
399
|
+
this._cleanup();
|
|
400
|
+
this._setState(ConnectionState.DISCONNECTED);
|
|
401
|
+
this._emit(BuiltInEvents.CLOSE, { manual: false });
|
|
402
|
+
|
|
403
|
+
// Schedule reconnection if enabled and not manually disconnected
|
|
404
|
+
if (this._options.reconnect && !this._manualDisconnect) {
|
|
405
|
+
this._scheduleReconnect();
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Schedule a reconnection attempt with exponential backoff and jitter.
|
|
411
|
+
* @private
|
|
412
|
+
*/
|
|
413
|
+
_scheduleReconnect() {
|
|
414
|
+
if (this._manualDisconnect) {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (this._reconnectAttempts >= this._options.maxReconnectAttempts) {
|
|
419
|
+
this._emit(BuiltInEvents.ERROR, {
|
|
420
|
+
message: 'Max reconnection attempts reached',
|
|
421
|
+
attempts: this._reconnectAttempts
|
|
422
|
+
});
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Exponential backoff: interval * 2^attempts
|
|
427
|
+
const exponentialDelay = this._options.reconnectInterval * Math.pow(2, this._reconnectAttempts);
|
|
428
|
+
|
|
429
|
+
// Cap at max delay
|
|
430
|
+
const cappedDelay = Math.min(exponentialDelay, this._options.maxReconnectDelay);
|
|
431
|
+
|
|
432
|
+
// Add jitter (0-1000ms random) to prevent thundering herd
|
|
433
|
+
const jitter = Math.random() * 1000;
|
|
434
|
+
const finalDelay = cappedDelay + jitter;
|
|
435
|
+
|
|
436
|
+
this._reconnectTimer = setTimeout(() => {
|
|
437
|
+
this._reconnectAttempts++;
|
|
438
|
+
this.connect();
|
|
439
|
+
}, finalDelay);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Register EventSource listeners for all subscribed event types.
|
|
444
|
+
* @private
|
|
445
|
+
*/
|
|
446
|
+
_registerEventListeners() {
|
|
447
|
+
const events = this._subscriptions.getEvents();
|
|
448
|
+
|
|
449
|
+
for (const event of events) {
|
|
450
|
+
if (!this._isBuiltInEvent(event)) {
|
|
451
|
+
this._registerSingleEventListener(event);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Register a single event listener on the EventSource.
|
|
458
|
+
* @private
|
|
459
|
+
*/
|
|
460
|
+
_registerSingleEventListener(event) {
|
|
461
|
+
if (!this._eventSource || this._registeredEventTypes.has(event)) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
this._registeredEventTypes.add(event);
|
|
466
|
+
|
|
467
|
+
this._eventSource.addEventListener(event, (sseEvent) => {
|
|
468
|
+
this._handleMessage(event, sseEvent);
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Handle incoming SSE message.
|
|
474
|
+
* @private
|
|
475
|
+
*/
|
|
476
|
+
_handleMessage(eventType, sseEvent) {
|
|
477
|
+
let data;
|
|
478
|
+
|
|
479
|
+
try {
|
|
480
|
+
// Attempt JSON parsing
|
|
481
|
+
data = JSON.parse(sseEvent.data);
|
|
482
|
+
} catch (parseError) {
|
|
483
|
+
// JSON parsing failed - emit error but don't disconnect
|
|
484
|
+
this._emit(BuiltInEvents.ERROR, {
|
|
485
|
+
message: 'Failed to parse JSON payload',
|
|
486
|
+
eventType,
|
|
487
|
+
originalData: sseEvent.data,
|
|
488
|
+
error: parseError.message
|
|
489
|
+
});
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Emit the parsed event to subscribers
|
|
494
|
+
this._emit(eventType, data);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Check if an event name is a built-in event.
|
|
499
|
+
* @private
|
|
500
|
+
*/
|
|
501
|
+
_isBuiltInEvent(event) {
|
|
502
|
+
return event.startsWith('stream.');
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Clean up resources.
|
|
507
|
+
* @private
|
|
508
|
+
*/
|
|
509
|
+
_cleanup() {
|
|
510
|
+
// Clear reconnection timer
|
|
511
|
+
if (this._reconnectTimer) {
|
|
512
|
+
clearTimeout(this._reconnectTimer);
|
|
513
|
+
this._reconnectTimer = null;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Close EventSource
|
|
517
|
+
if (this._eventSource) {
|
|
518
|
+
this._eventSource.close();
|
|
519
|
+
this._eventSource = null;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Clear registered event types (will be re-registered on reconnect)
|
|
523
|
+
this._registeredEventTypes.clear();
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
export { BuiltInEvents, ConnectionState, DefaultOptions, EventClient, SubscriptionManager };
|
|
528
|
+
//# sourceMappingURL=pushstream.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pushstream.esm.js","sources":["../src/constants.js","../src/SubscriptionManager.js","../src/EventClient.js"],"sourcesContent":["/**\r\n * Connection states for EventClient\r\n * @readonly\r\n * @enum {string}\r\n */\r\nexport const ConnectionState = Object.freeze({\r\n /** Client is not connected */\r\n DISCONNECTED: 'disconnected',\r\n /** Client is attempting to connect */\r\n CONNECTING: 'connecting',\r\n /** Client is connected and receiving events */\r\n CONNECTED: 'connected'\r\n});\r\n\r\n/**\r\n * Built-in event names emitted by EventClient\r\n * @readonly\r\n * @enum {string}\r\n */\r\nexport const BuiltInEvents = Object.freeze({\r\n /** Emitted when connection is established */\r\n OPEN: 'stream.open',\r\n /** Emitted when connection is closed */\r\n CLOSE: 'stream.close',\r\n /** Emitted when an error occurs */\r\n ERROR: 'stream.error',\r\n /** Emitted when connection state changes */\r\n STATE_CHANGE: 'stream.statechange'\r\n});\r\n\r\n/**\r\n * Default options for EventClient\r\n * @readonly\r\n */\r\nexport const DefaultOptions = Object.freeze({\r\n /** Whether to automatically reconnect on connection loss */\r\n reconnect: true,\r\n /** Base delay in milliseconds between reconnection attempts */\r\n reconnectInterval: 1000,\r\n /** Maximum number of reconnection attempts before giving up */\r\n maxReconnectAttempts: 10,\r\n /** Maximum delay cap for exponential backoff (30 seconds) */\r\n maxReconnectDelay: 30000,\r\n /** Whether to include credentials in cross-origin requests */\r\n withCredentials: false\r\n});\r\n\r\n","/**\r\n * Manages event subscriptions with O(1) add/remove operations.\r\n * Uses Map for event->callbacks storage and Set for callback deduplication.\r\n */\r\nexport class SubscriptionManager {\r\n constructor() {\r\n /** @type {Map<string, Set<Function>>} */\r\n this._listeners = new Map();\r\n }\r\n\r\n /**\r\n * Register a callback for a specific event.\r\n * @param {string} event - The event name to subscribe to\r\n * @param {Function} callback - The callback to invoke when the event occurs\r\n * @returns {boolean} True if the callback was added, false if already exists\r\n */\r\n add(event, callback) {\r\n if (typeof callback !== 'function') {\r\n throw new TypeError('Callback must be a function');\r\n }\r\n\r\n if (!this._listeners.has(event)) {\r\n this._listeners.set(event, new Set());\r\n }\r\n\r\n const callbacks = this._listeners.get(event);\r\n const existed = callbacks.has(callback);\r\n callbacks.add(callback);\r\n \r\n return !existed;\r\n }\r\n\r\n /**\r\n * Remove a specific callback for an event.\r\n * @param {string} event - The event name\r\n * @param {Function} callback - The callback to remove\r\n * @returns {boolean} True if the callback was removed\r\n */\r\n remove(event, callback) {\r\n const callbacks = this._listeners.get(event);\r\n if (!callbacks) {\r\n return false;\r\n }\r\n\r\n const removed = callbacks.delete(callback);\r\n \r\n // Clean up empty sets\r\n if (callbacks.size === 0) {\r\n this._listeners.delete(event);\r\n }\r\n\r\n return removed;\r\n }\r\n\r\n /**\r\n * Remove all callbacks for a specific event.\r\n * @param {string} event - The event name\r\n * @returns {boolean} True if any callbacks were removed\r\n */\r\n removeAll(event) {\r\n return this._listeners.delete(event);\r\n }\r\n\r\n /**\r\n * Emit an event to all registered callbacks.\r\n * Creates a snapshot of callbacks to allow safe modification during iteration.\r\n * @param {string} event - The event name\r\n * @param {*} data - The data to pass to callbacks\r\n */\r\n emit(event, data) {\r\n const callbacks = this._listeners.get(event);\r\n if (!callbacks || callbacks.size === 0) {\r\n return;\r\n }\r\n\r\n // Create snapshot to allow modifications during iteration\r\n const snapshot = Array.from(callbacks);\r\n \r\n for (const callback of snapshot) {\r\n try {\r\n callback(data);\r\n } catch (error) {\r\n // Log but don't throw to prevent one bad callback from breaking others\r\n console.error(`Error in event callback for \"${event}\":`, error);\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * Check if an event has any subscribers.\r\n * @param {string} event - The event name\r\n * @returns {boolean} True if the event has subscribers\r\n */\r\n has(event) {\r\n const callbacks = this._listeners.get(event);\r\n return callbacks !== undefined && callbacks.size > 0;\r\n }\r\n\r\n /**\r\n * Get all registered event names.\r\n * @returns {string[]} Array of event names\r\n */\r\n getEvents() {\r\n return Array.from(this._listeners.keys());\r\n }\r\n\r\n /**\r\n * Get the number of callbacks for a specific event.\r\n * @param {string} event - The event name\r\n * @returns {number} Number of callbacks\r\n */\r\n getCount(event) {\r\n const callbacks = this._listeners.get(event);\r\n return callbacks ? callbacks.size : 0;\r\n }\r\n\r\n /**\r\n * Clear all subscriptions.\r\n */\r\n clear() {\r\n this._listeners.clear();\r\n }\r\n}\r\n\r\n","import { ConnectionState, BuiltInEvents, DefaultOptions } from './constants.js';\r\nimport { SubscriptionManager } from './SubscriptionManager.js';\r\n\r\n/**\r\n * EventClient provides a clean abstraction over EventSource for consuming SSE events.\r\n * \r\n * Features:\r\n * - Automatic reconnection with exponential backoff and jitter\r\n * - Event subscription management\r\n * - Automatic JSON payload parsing\r\n * - Connection state tracking\r\n * - Built-in lifecycle events\r\n * \r\n * @example\r\n * const client = new EventClient('/events');\r\n * client.on('task.progress', (data) => console.log(data.percentage));\r\n * client.connect();\r\n */\r\nexport class EventClient {\r\n /**\r\n * Create a new EventClient instance.\r\n * @param {string} url - The SSE endpoint URL (relative or absolute)\r\n * @param {Object} [options] - Configuration options\r\n * @param {boolean} [options.reconnect=true] - Enable automatic reconnection\r\n * @param {number} [options.reconnectInterval=1000] - Base reconnection delay in ms\r\n * @param {number} [options.maxReconnectAttempts=10] - Maximum reconnection attempts\r\n * @param {number} [options.maxReconnectDelay=30000] - Maximum delay cap in ms\r\n * @param {boolean} [options.withCredentials=false] - Include credentials in CORS requests\r\n */\r\n constructor(url, options = {}) {\r\n if (!url || typeof url !== 'string') {\r\n throw new TypeError('URL must be a non-empty string');\r\n }\r\n\r\n this._url = url;\r\n this._options = { ...DefaultOptions, ...options };\r\n this._eventSource = null;\r\n this._subscriptions = new SubscriptionManager();\r\n this._state = ConnectionState.DISCONNECTED;\r\n this._reconnectAttempts = 0;\r\n this._reconnectTimer = null;\r\n this._manualDisconnect = false;\r\n this._registeredEventTypes = new Set();\r\n }\r\n\r\n /**\r\n * Get the current connection state.\r\n * @returns {string} One of: 'disconnected', 'connecting', 'connected'\r\n */\r\n get state() {\r\n return this._state;\r\n }\r\n\r\n /**\r\n * Get the endpoint URL.\r\n * @returns {string} The SSE endpoint URL\r\n */\r\n get url() {\r\n return this._url;\r\n }\r\n\r\n /**\r\n * Establish an SSE connection to the server.\r\n * This method is idempotent - calling it while already connected has no effect.\r\n */\r\n connect() {\r\n // Idempotent: don't reconnect if already connecting or connected\r\n if (this._state !== ConnectionState.DISCONNECTED) {\r\n return;\r\n }\r\n\r\n this._manualDisconnect = false;\r\n this._setState(ConnectionState.CONNECTING);\r\n\r\n try {\r\n this._eventSource = new EventSource(this._url, {\r\n withCredentials: this._options.withCredentials\r\n });\r\n\r\n this._eventSource.onopen = () => this._handleOpen();\r\n this._eventSource.onerror = (event) => this._handleError(event);\r\n\r\n // Register event listeners for all currently subscribed event types\r\n this._registerEventListeners();\r\n } catch (error) {\r\n this._handleConnectionError(error);\r\n }\r\n }\r\n\r\n /**\r\n * Close the SSE connection.\r\n * This method is idempotent - calling it while already disconnected has no effect.\r\n * After calling disconnect(), no automatic reconnection will be attempted.\r\n */\r\n disconnect() {\r\n this._manualDisconnect = true;\r\n this._cleanup();\r\n \r\n if (this._state !== ConnectionState.DISCONNECTED) {\r\n this._setState(ConnectionState.DISCONNECTED);\r\n this._emit(BuiltInEvents.CLOSE, { manual: true });\r\n }\r\n }\r\n\r\n /**\r\n * Subscribe to an event.\r\n * Subscriptions can be registered before or after connecting.\r\n * @param {string} event - The event name to subscribe to\r\n * @param {Function} callback - The callback to invoke when the event occurs\r\n * @returns {EventClient} This instance for chaining\r\n */\r\n on(event, callback) {\r\n if (typeof event !== 'string' || !event) {\r\n throw new TypeError('Event name must be a non-empty string');\r\n }\r\n if (typeof callback !== 'function') {\r\n throw new TypeError('Callback must be a function');\r\n }\r\n\r\n this._subscriptions.add(event, callback);\r\n\r\n // If already connected and this is a new event type, register it\r\n if (this._eventSource && !this._isBuiltInEvent(event) && !this._registeredEventTypes.has(event)) {\r\n this._registerSingleEventListener(event);\r\n }\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Unsubscribe from an event.\r\n * @param {string} event - The event name\r\n * @param {Function} [callback] - Specific callback to remove. If omitted, removes all callbacks for the event.\r\n * @returns {EventClient} This instance for chaining\r\n */\r\n off(event, callback) {\r\n if (typeof event !== 'string' || !event) {\r\n throw new TypeError('Event name must be a non-empty string');\r\n }\r\n\r\n if (callback !== undefined) {\r\n this._subscriptions.remove(event, callback);\r\n } else {\r\n this._subscriptions.removeAll(event);\r\n }\r\n\r\n return this;\r\n }\r\n\r\n // =====================\r\n // Private Methods\r\n // =====================\r\n\r\n /**\r\n * Update connection state and emit state change event.\r\n * @private\r\n */\r\n _setState(newState) {\r\n const oldState = this._state;\r\n if (oldState === newState) {\r\n return;\r\n }\r\n\r\n this._state = newState;\r\n this._emit(BuiltInEvents.STATE_CHANGE, {\r\n previousState: oldState,\r\n currentState: newState\r\n });\r\n }\r\n\r\n /**\r\n * Emit an event to all subscribers.\r\n * @private\r\n */\r\n _emit(event, data) {\r\n this._subscriptions.emit(event, data);\r\n }\r\n\r\n /**\r\n * Handle successful connection.\r\n * @private\r\n */\r\n _handleOpen() {\r\n this._reconnectAttempts = 0; // Reset on successful connection\r\n this._setState(ConnectionState.CONNECTED);\r\n this._emit(BuiltInEvents.OPEN, { url: this._url });\r\n }\r\n\r\n /**\r\n * Handle connection error.\r\n * @private\r\n */\r\n _handleError(event) {\r\n // EventSource error event doesn't provide much detail\r\n const errorInfo = {\r\n message: 'Connection error',\r\n readyState: this._eventSource?.readyState\r\n };\r\n\r\n this._emit(BuiltInEvents.ERROR, errorInfo);\r\n\r\n // Check if connection was lost\r\n if (this._eventSource?.readyState === EventSource.CLOSED) {\r\n this._handleConnectionLoss();\r\n }\r\n }\r\n\r\n /**\r\n * Handle initial connection failure.\r\n * @private\r\n */\r\n _handleConnectionError(error) {\r\n this._setState(ConnectionState.DISCONNECTED);\r\n this._emit(BuiltInEvents.ERROR, {\r\n message: error.message || 'Failed to create connection',\r\n error\r\n });\r\n }\r\n\r\n /**\r\n * Handle connection loss and schedule reconnection.\r\n * @private\r\n */\r\n _handleConnectionLoss() {\r\n this._cleanup();\r\n this._setState(ConnectionState.DISCONNECTED);\r\n this._emit(BuiltInEvents.CLOSE, { manual: false });\r\n\r\n // Schedule reconnection if enabled and not manually disconnected\r\n if (this._options.reconnect && !this._manualDisconnect) {\r\n this._scheduleReconnect();\r\n }\r\n }\r\n\r\n /**\r\n * Schedule a reconnection attempt with exponential backoff and jitter.\r\n * @private\r\n */\r\n _scheduleReconnect() {\r\n if (this._manualDisconnect) {\r\n return;\r\n }\r\n\r\n if (this._reconnectAttempts >= this._options.maxReconnectAttempts) {\r\n this._emit(BuiltInEvents.ERROR, {\r\n message: 'Max reconnection attempts reached',\r\n attempts: this._reconnectAttempts\r\n });\r\n return;\r\n }\r\n\r\n // Exponential backoff: interval * 2^attempts\r\n const exponentialDelay = this._options.reconnectInterval * Math.pow(2, this._reconnectAttempts);\r\n \r\n // Cap at max delay\r\n const cappedDelay = Math.min(exponentialDelay, this._options.maxReconnectDelay);\r\n \r\n // Add jitter (0-1000ms random) to prevent thundering herd\r\n const jitter = Math.random() * 1000;\r\n const finalDelay = cappedDelay + jitter;\r\n\r\n this._reconnectTimer = setTimeout(() => {\r\n this._reconnectAttempts++;\r\n this.connect();\r\n }, finalDelay);\r\n }\r\n\r\n /**\r\n * Register EventSource listeners for all subscribed event types.\r\n * @private\r\n */\r\n _registerEventListeners() {\r\n const events = this._subscriptions.getEvents();\r\n \r\n for (const event of events) {\r\n if (!this._isBuiltInEvent(event)) {\r\n this._registerSingleEventListener(event);\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * Register a single event listener on the EventSource.\r\n * @private\r\n */\r\n _registerSingleEventListener(event) {\r\n if (!this._eventSource || this._registeredEventTypes.has(event)) {\r\n return;\r\n }\r\n\r\n this._registeredEventTypes.add(event);\r\n \r\n this._eventSource.addEventListener(event, (sseEvent) => {\r\n this._handleMessage(event, sseEvent);\r\n });\r\n }\r\n\r\n /**\r\n * Handle incoming SSE message.\r\n * @private\r\n */\r\n _handleMessage(eventType, sseEvent) {\r\n let data;\r\n \r\n try {\r\n // Attempt JSON parsing\r\n data = JSON.parse(sseEvent.data);\r\n } catch (parseError) {\r\n // JSON parsing failed - emit error but don't disconnect\r\n this._emit(BuiltInEvents.ERROR, {\r\n message: 'Failed to parse JSON payload',\r\n eventType,\r\n originalData: sseEvent.data,\r\n error: parseError.message\r\n });\r\n return;\r\n }\r\n\r\n // Emit the parsed event to subscribers\r\n this._emit(eventType, data);\r\n }\r\n\r\n /**\r\n * Check if an event name is a built-in event.\r\n * @private\r\n */\r\n _isBuiltInEvent(event) {\r\n return event.startsWith('stream.');\r\n }\r\n\r\n /**\r\n * Clean up resources.\r\n * @private\r\n */\r\n _cleanup() {\r\n // Clear reconnection timer\r\n if (this._reconnectTimer) {\r\n clearTimeout(this._reconnectTimer);\r\n this._reconnectTimer = null;\r\n }\r\n\r\n // Close EventSource\r\n if (this._eventSource) {\r\n this._eventSource.close();\r\n this._eventSource = null;\r\n }\r\n\r\n // Clear registered event types (will be re-registered on reconnect)\r\n this._registeredEventTypes.clear();\r\n }\r\n}\r\n\r\n"],"names":[],"mappings":"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"}
|