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