@eleven-am/pondsocket-client 0.0.30 → 0.0.32

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/browser/client.js CHANGED
@@ -10,13 +10,17 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
10
10
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
11
11
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
12
12
  };
13
- var _PondClient_instances, _PondClient_channels, _PondClient_createPublisher, _PondClient_handleAcknowledge, _PondClient_init;
13
+ var _PondClient_instances, _PondClient_channels, _PondClient_clearTimeouts, _PondClient_createPublisher, _PondClient_handleAcknowledge, _PondClient_handleUnauthorized, _PondClient_init;
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.PondClient = void 0;
16
16
  const pondsocket_common_1 = require("@eleven-am/pondsocket-common");
17
17
  const channel_1 = require("../core/channel");
18
+ const types_1 = require("../types");
19
+ const DEFAULT_CONNECTION_TIMEOUT = 10000;
20
+ const DEFAULT_MAX_RECONNECT_DELAY = 30000;
18
21
  class PondClient {
19
- constructor(endpoint, params = {}) {
22
+ constructor(endpoint, params = {}, options = {}) {
23
+ var _a, _b;
20
24
  _PondClient_instances.add(this);
21
25
  _PondClient_channels.set(this, void 0);
22
26
  let address;
@@ -28,6 +32,7 @@ class PondClient {
28
32
  address.pathname = endpoint;
29
33
  }
30
34
  this._disconnecting = false;
35
+ this._reconnectAttempts = 0;
31
36
  const query = new URLSearchParams(params);
32
37
  address.search = query.toString();
33
38
  const protocol = address.protocol === 'https:' ? 'wss:' : 'ws:';
@@ -35,9 +40,15 @@ class PondClient {
35
40
  address.protocol = protocol;
36
41
  }
37
42
  this._address = address;
43
+ this._options = {
44
+ connectionTimeout: (_a = options.connectionTimeout) !== null && _a !== void 0 ? _a : DEFAULT_CONNECTION_TIMEOUT,
45
+ maxReconnectDelay: (_b = options.maxReconnectDelay) !== null && _b !== void 0 ? _b : DEFAULT_MAX_RECONNECT_DELAY,
46
+ pingInterval: options.pingInterval,
47
+ };
38
48
  __classPrivateFieldSet(this, _PondClient_channels, new Map(), "f");
39
49
  this._broadcaster = new pondsocket_common_1.Subject();
40
- this._connectionState = new pondsocket_common_1.BehaviorSubject(false);
50
+ this._connectionState = new pondsocket_common_1.BehaviorSubject(types_1.ConnectionState.DISCONNECTED);
51
+ this._errorSubject = new pondsocket_common_1.Subject();
41
52
  __classPrivateFieldGet(this, _PondClient_instances, "m", _PondClient_init).call(this);
42
53
  }
43
54
  /**
@@ -45,7 +56,26 @@ class PondClient {
45
56
  */
46
57
  connect() {
47
58
  this._disconnecting = false;
59
+ this._connectionState.publish(types_1.ConnectionState.CONNECTING);
48
60
  const socket = new WebSocket(this._address.toString());
61
+ this._connectionTimeoutId = setTimeout(() => {
62
+ if (socket.readyState === WebSocket.CONNECTING) {
63
+ const error = new Error('Connection timeout');
64
+ this._errorSubject.publish(error);
65
+ socket.close();
66
+ }
67
+ }, this._options.connectionTimeout);
68
+ socket.onopen = () => {
69
+ __classPrivateFieldGet(this, _PondClient_instances, "m", _PondClient_clearTimeouts).call(this);
70
+ this._reconnectAttempts = 0;
71
+ if (this._options.pingInterval) {
72
+ this._pingIntervalId = setInterval(() => {
73
+ if (socket.readyState === WebSocket.OPEN) {
74
+ socket.send(JSON.stringify({ action: 'ping' }));
75
+ }
76
+ }, this._options.pingInterval);
77
+ }
78
+ };
49
79
  socket.onmessage = (message) => {
50
80
  const lines = message.data.trim().split('\n');
51
81
  for (const line of lines) {
@@ -56,15 +86,22 @@ class PondClient {
56
86
  }
57
87
  }
58
88
  };
59
- socket.onerror = () => socket.close();
89
+ socket.onerror = (event) => {
90
+ const error = new Error('WebSocket error');
91
+ this._errorSubject.publish(error);
92
+ socket.close();
93
+ };
60
94
  socket.onclose = () => {
61
- this._connectionState.publish(false);
95
+ __classPrivateFieldGet(this, _PondClient_instances, "m", _PondClient_clearTimeouts).call(this);
96
+ this._connectionState.publish(types_1.ConnectionState.DISCONNECTED);
62
97
  if (this._disconnecting) {
63
98
  return;
64
99
  }
100
+ const delay = Math.min(1000 * Math.pow(2, this._reconnectAttempts), this._options.maxReconnectDelay);
101
+ this._reconnectAttempts++;
65
102
  setTimeout(() => {
66
103
  this.connect();
67
- }, 1000);
104
+ }, delay);
68
105
  };
69
106
  this._socket = socket;
70
107
  }
@@ -79,8 +116,9 @@ class PondClient {
79
116
  */
80
117
  disconnect() {
81
118
  var _a;
82
- this._connectionState.publish(false);
119
+ __classPrivateFieldGet(this, _PondClient_instances, "m", _PondClient_clearTimeouts).call(this);
83
120
  this._disconnecting = true;
121
+ this._connectionState.publish(types_1.ConnectionState.DISCONNECTED);
84
122
  (_a = this._socket) === null || _a === void 0 ? void 0 : _a.close();
85
123
  __classPrivateFieldGet(this, _PondClient_channels, "f").clear();
86
124
  }
@@ -106,11 +144,27 @@ class PondClient {
106
144
  onConnectionChange(callback) {
107
145
  return this._connectionState.subscribe(callback);
108
146
  }
147
+ /**
148
+ * @desc Subscribes to connection errors.
149
+ * @param callback - The callback to call when an error occurs.
150
+ */
151
+ onError(callback) {
152
+ return this._errorSubject.subscribe(callback);
153
+ }
109
154
  }
110
155
  exports.PondClient = PondClient;
111
- _PondClient_channels = new WeakMap(), _PondClient_instances = new WeakSet(), _PondClient_createPublisher = function _PondClient_createPublisher() {
156
+ _PondClient_channels = new WeakMap(), _PondClient_instances = new WeakSet(), _PondClient_clearTimeouts = function _PondClient_clearTimeouts() {
157
+ if (this._connectionTimeoutId) {
158
+ clearTimeout(this._connectionTimeoutId);
159
+ this._connectionTimeoutId = undefined;
160
+ }
161
+ if (this._pingIntervalId) {
162
+ clearInterval(this._pingIntervalId);
163
+ this._pingIntervalId = undefined;
164
+ }
165
+ }, _PondClient_createPublisher = function _PondClient_createPublisher() {
112
166
  return (message) => {
113
- if (this._connectionState.value) {
167
+ if (this._connectionState.value === types_1.ConnectionState.CONNECTED) {
114
168
  this._socket.send(JSON.stringify(message));
115
169
  }
116
170
  };
@@ -119,13 +173,22 @@ _PondClient_channels = new WeakMap(), _PondClient_instances = new WeakSet(), _Po
119
173
  const channel = (_a = __classPrivateFieldGet(this, _PondClient_channels, "f").get(message.channelName)) !== null && _a !== void 0 ? _a : new channel_1.Channel(__classPrivateFieldGet(this, _PondClient_instances, "m", _PondClient_createPublisher).call(this), this._connectionState, message.channelName, {});
120
174
  __classPrivateFieldGet(this, _PondClient_channels, "f").set(message.channelName, channel);
121
175
  channel.acknowledge(this._broadcaster);
176
+ }, _PondClient_handleUnauthorized = function _PondClient_handleUnauthorized(message) {
177
+ const channel = __classPrivateFieldGet(this, _PondClient_channels, "f").get(message.channelName);
178
+ if (channel) {
179
+ const payload = message.payload;
180
+ channel.decline(payload);
181
+ }
122
182
  }, _PondClient_init = function _PondClient_init() {
123
183
  this._broadcaster.subscribe((message) => {
124
184
  if (message.event === pondsocket_common_1.Events.ACKNOWLEDGE) {
125
185
  __classPrivateFieldGet(this, _PondClient_instances, "m", _PondClient_handleAcknowledge).call(this, message);
126
186
  }
187
+ else if (message.event === pondsocket_common_1.Events.UNAUTHORIZED) {
188
+ __classPrivateFieldGet(this, _PondClient_instances, "m", _PondClient_handleUnauthorized).call(this, message);
189
+ }
127
190
  else if (message.event === pondsocket_common_1.Events.CONNECTION && message.action === pondsocket_common_1.ServerActions.CONNECT) {
128
- this._connectionState.publish(true);
191
+ this._connectionState.publish(types_1.ConnectionState.CONNECTED);
129
192
  }
130
193
  });
131
194
  };
@@ -11,14 +11,18 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  const pondsocket_common_1 = require("@eleven-am/pondsocket-common");
13
13
  const client_1 = require("./client");
14
+ const types_1 = require("../types");
14
15
  class MockWebSocket {
15
16
  // eslint-disable-next-line no-useless-constructor
16
17
  constructor(url) {
17
18
  this.url = url;
18
19
  this.send = jest.fn();
19
20
  this.close = jest.fn();
21
+ this.readyState = MockWebSocket.CONNECTING;
20
22
  }
21
23
  }
24
+ MockWebSocket.CONNECTING = 0;
25
+ MockWebSocket.OPEN = 1;
22
26
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
23
27
  // @ts-expect-error
24
28
  global.WebSocket = MockWebSocket;
@@ -69,11 +73,12 @@ describe('PondClient', () => {
69
73
  expect(broadcasterSpy).not.toHaveBeenCalled();
70
74
  });
71
75
  test('socket should only pass to publish state when acknowledged event is received', () => {
72
- pondClient.connect();
73
- const mockWebSocket = pondClient['_socket'];
74
76
  const mockCallback = jest.fn();
75
77
  pondClient.onConnectionChange(mockCallback);
76
- expect(mockCallback).not.toHaveBeenCalled();
78
+ mockCallback.mockClear();
79
+ pondClient.connect();
80
+ const mockWebSocket = pondClient['_socket'];
81
+ expect(mockCallback).toHaveBeenCalledWith(types_1.ConnectionState.CONNECTING);
77
82
  const acknowledgeEvent = {
78
83
  event: pondsocket_common_1.Events.CONNECTION,
79
84
  action: pondsocket_common_1.ServerActions.CONNECT,
@@ -82,7 +87,7 @@ describe('PondClient', () => {
82
87
  payload: {},
83
88
  };
84
89
  mockWebSocket.onmessage({ data: JSON.stringify(acknowledgeEvent) });
85
- expect(mockCallback).toHaveBeenCalledWith(true);
90
+ expect(mockCallback).toHaveBeenCalledWith(types_1.ConnectionState.CONNECTED);
86
91
  });
87
92
  test('createChannel method should create a new channel or return an existing one', () => {
88
93
  const mockChannel = pondClient.createChannel('exampleChannel');
@@ -94,19 +99,20 @@ describe('PondClient', () => {
94
99
  test('onConnectionChange method should subscribe to connection state changes', () => {
95
100
  const mockCallback = jest.fn();
96
101
  const unsubscribe = pondClient.onConnectionChange(mockCallback);
97
- pondClient['_connectionState'].publish(true);
98
- expect(mockCallback).toHaveBeenCalledWith(true);
102
+ mockCallback.mockClear();
103
+ pondClient['_connectionState'].publish(types_1.ConnectionState.CONNECTED);
104
+ expect(mockCallback).toHaveBeenCalledWith(types_1.ConnectionState.CONNECTED);
99
105
  unsubscribe();
100
- pondClient['_connectionState'].publish(false);
106
+ pondClient['_connectionState'].publish(types_1.ConnectionState.DISCONNECTED);
101
107
  // Should not be called again after unsubscribe
102
108
  expect(mockCallback).toHaveBeenCalledTimes(1);
103
109
  });
104
110
  test('getState method should return the current state of the socket', () => {
105
111
  const initialState = pondClient.getState();
106
- expect(initialState).toBe(false);
107
- pondClient['_connectionState'].publish(true);
112
+ expect(initialState).toBe(types_1.ConnectionState.DISCONNECTED);
113
+ pondClient['_connectionState'].publish(types_1.ConnectionState.CONNECTED);
108
114
  const updatedState = pondClient.getState();
109
- expect(updatedState).toBe(true);
115
+ expect(updatedState).toBe(types_1.ConnectionState.CONNECTED);
110
116
  });
111
117
  test('publish method should send a message to the server', () => {
112
118
  pondClient.connect();
@@ -131,7 +137,7 @@ describe('PondClient', () => {
131
137
  channelName: 'exampleChannel',
132
138
  }));
133
139
  });
134
- it('onError method should reconnect to the server', () => __awaiter(void 0, void 0, void 0, function* () {
140
+ it('should reconnect with exponential backoff', () => __awaiter(void 0, void 0, void 0, function* () {
135
141
  const connectSpy = jest.spyOn(pondClient, 'connect');
136
142
  pondClient.connect();
137
143
  let mockWebSocket = pondClient['_socket'];
@@ -146,4 +152,56 @@ describe('PondClient', () => {
146
152
  yield new Promise((resolve) => setTimeout(resolve, 2000));
147
153
  expect(connectSpy).toHaveBeenCalledTimes(1);
148
154
  }));
155
+ test('onError method should subscribe to error events', () => {
156
+ const errorCallback = jest.fn();
157
+ pondClient.onError(errorCallback);
158
+ pondClient.connect();
159
+ const mockWebSocket = pondClient['_socket'];
160
+ mockWebSocket.onerror({ type: 'error' });
161
+ expect(errorCallback).toHaveBeenCalledTimes(1);
162
+ expect(errorCallback).toHaveBeenCalledWith(expect.any(Error));
163
+ });
164
+ test('connect should transition through connection states', () => {
165
+ const stateCallback = jest.fn();
166
+ pondClient.onConnectionChange(stateCallback);
167
+ expect(pondClient.getState()).toBe(types_1.ConnectionState.DISCONNECTED);
168
+ pondClient.connect();
169
+ expect(stateCallback).toHaveBeenCalledWith(types_1.ConnectionState.CONNECTING);
170
+ });
171
+ test('connection timeout should trigger error and close', () => __awaiter(void 0, void 0, void 0, function* () {
172
+ jest.useFakeTimers();
173
+ const clientWithTimeout = new client_1.PondClient('ws://example.com', {}, { connectionTimeout: 5000 });
174
+ const errorCallback = jest.fn();
175
+ clientWithTimeout.onError(errorCallback);
176
+ clientWithTimeout.connect();
177
+ const mockWebSocket = clientWithTimeout['_socket'];
178
+ mockWebSocket.readyState = MockWebSocket.CONNECTING;
179
+ jest.advanceTimersByTime(5000);
180
+ expect(errorCallback).toHaveBeenCalledWith(expect.objectContaining({ message: 'Connection timeout' }));
181
+ expect(mockWebSocket.close).toHaveBeenCalled();
182
+ jest.useRealTimers();
183
+ }));
184
+ test('ping interval should send ping messages when connected', () => __awaiter(void 0, void 0, void 0, function* () {
185
+ jest.useFakeTimers();
186
+ const clientWithPing = new client_1.PondClient('ws://example.com', {}, { pingInterval: 1000 });
187
+ clientWithPing.connect();
188
+ const mockWebSocket = clientWithPing['_socket'];
189
+ mockWebSocket.readyState = MockWebSocket.OPEN;
190
+ mockWebSocket.onopen();
191
+ jest.advanceTimersByTime(1000);
192
+ expect(mockWebSocket.send).toHaveBeenCalledWith(JSON.stringify({ action: 'ping' }));
193
+ jest.advanceTimersByTime(1000);
194
+ expect(mockWebSocket.send).toHaveBeenCalledTimes(2);
195
+ jest.useRealTimers();
196
+ }));
197
+ test('disconnect should clear timeouts and stop reconnection', () => {
198
+ pondClient.connect();
199
+ const mockWebSocket = pondClient['_socket'];
200
+ pondClient.disconnect();
201
+ expect(pondClient.getState()).toBe(types_1.ConnectionState.DISCONNECTED);
202
+ expect(mockWebSocket.close).toHaveBeenCalled();
203
+ const connectSpy = jest.spyOn(pondClient, 'connect');
204
+ mockWebSocket.onclose();
205
+ expect(connectSpy).not.toHaveBeenCalled();
206
+ });
149
207
  });
@@ -0,0 +1,199 @@
1
+ "use strict";
2
+ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
3
+ if (kind === "m") throw new TypeError("Private method is not writable");
4
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
5
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
6
+ return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
7
+ };
8
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
9
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
10
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
11
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
12
+ };
13
+ var _SSEClient_instances, _SSEClient_channels, _SSEClient_handleMessage, _SSEClient_clearTimeout, _SSEClient_createPublisher, _SSEClient_handleAcknowledge, _SSEClient_handleUnauthorized, _SSEClient_init;
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.SSEClient = void 0;
16
+ const pondsocket_common_1 = require("@eleven-am/pondsocket-common");
17
+ const channel_1 = require("../core/channel");
18
+ const types_1 = require("../types");
19
+ const DEFAULT_CONNECTION_TIMEOUT = 10000;
20
+ const DEFAULT_MAX_RECONNECT_DELAY = 30000;
21
+ class SSEClient {
22
+ constructor(endpoint, params = {}, options = {}) {
23
+ var _a, _b;
24
+ _SSEClient_instances.add(this);
25
+ _SSEClient_channels.set(this, void 0);
26
+ let address;
27
+ try {
28
+ address = new URL(endpoint);
29
+ }
30
+ catch (e) {
31
+ address = new URL(window.location.toString());
32
+ address.pathname = endpoint;
33
+ }
34
+ this._disconnecting = false;
35
+ this._reconnectAttempts = 0;
36
+ const query = new URLSearchParams(params);
37
+ address.search = query.toString();
38
+ if (address.protocol !== 'https:' && address.protocol !== 'http:') {
39
+ address.protocol = window.location.protocol;
40
+ }
41
+ this._address = address;
42
+ this._postAddress = new URL(address.toString());
43
+ this._options = {
44
+ connectionTimeout: (_a = options.connectionTimeout) !== null && _a !== void 0 ? _a : DEFAULT_CONNECTION_TIMEOUT,
45
+ maxReconnectDelay: (_b = options.maxReconnectDelay) !== null && _b !== void 0 ? _b : DEFAULT_MAX_RECONNECT_DELAY,
46
+ pingInterval: options.pingInterval,
47
+ withCredentials: options.withCredentials,
48
+ };
49
+ __classPrivateFieldSet(this, _SSEClient_channels, new Map(), "f");
50
+ this._broadcaster = new pondsocket_common_1.Subject();
51
+ this._connectionState = new pondsocket_common_1.BehaviorSubject(types_1.ConnectionState.DISCONNECTED);
52
+ this._errorSubject = new pondsocket_common_1.Subject();
53
+ __classPrivateFieldGet(this, _SSEClient_instances, "m", _SSEClient_init).call(this);
54
+ }
55
+ connect() {
56
+ var _a;
57
+ this._disconnecting = false;
58
+ this._connectionState.publish(types_1.ConnectionState.CONNECTING);
59
+ const eventSource = new EventSource(this._address.toString(), {
60
+ withCredentials: (_a = this._options.withCredentials) !== null && _a !== void 0 ? _a : false,
61
+ });
62
+ this._connectionTimeoutId = setTimeout(() => {
63
+ if (eventSource.readyState === EventSource.CONNECTING) {
64
+ const error = new Error('Connection timeout');
65
+ this._errorSubject.publish(error);
66
+ eventSource.close();
67
+ }
68
+ }, this._options.connectionTimeout);
69
+ eventSource.onopen = () => {
70
+ __classPrivateFieldGet(this, _SSEClient_instances, "m", _SSEClient_clearTimeout).call(this);
71
+ this._reconnectAttempts = 0;
72
+ };
73
+ eventSource.onmessage = (event) => {
74
+ __classPrivateFieldGet(this, _SSEClient_instances, "m", _SSEClient_handleMessage).call(this, event.data);
75
+ };
76
+ eventSource.onerror = () => {
77
+ const error = new Error('SSE connection error');
78
+ this._errorSubject.publish(error);
79
+ eventSource.close();
80
+ __classPrivateFieldGet(this, _SSEClient_instances, "m", _SSEClient_clearTimeout).call(this);
81
+ this._connectionState.publish(types_1.ConnectionState.DISCONNECTED);
82
+ if (this._disconnecting) {
83
+ return;
84
+ }
85
+ const delay = Math.min(1000 * Math.pow(2, this._reconnectAttempts), this._options.maxReconnectDelay);
86
+ this._reconnectAttempts++;
87
+ setTimeout(() => {
88
+ this.connect();
89
+ }, delay);
90
+ };
91
+ this._eventSource = eventSource;
92
+ }
93
+ getState() {
94
+ return this._connectionState.value;
95
+ }
96
+ getConnectionId() {
97
+ return this._connectionId;
98
+ }
99
+ disconnect() {
100
+ var _a;
101
+ __classPrivateFieldGet(this, _SSEClient_instances, "m", _SSEClient_clearTimeout).call(this);
102
+ this._disconnecting = true;
103
+ this._connectionState.publish(types_1.ConnectionState.DISCONNECTED);
104
+ if (this._connectionId) {
105
+ fetch(this._postAddress.toString(), {
106
+ method: 'DELETE',
107
+ headers: {
108
+ 'X-Connection-ID': this._connectionId,
109
+ },
110
+ credentials: this._options.withCredentials ? 'include' : 'same-origin',
111
+ }).catch(() => {
112
+ });
113
+ }
114
+ (_a = this._eventSource) === null || _a === void 0 ? void 0 : _a.close();
115
+ this._connectionId = undefined;
116
+ __classPrivateFieldGet(this, _SSEClient_channels, "f").clear();
117
+ }
118
+ createChannel(name, params) {
119
+ const channel = __classPrivateFieldGet(this, _SSEClient_channels, "f").get(name);
120
+ if (channel && channel.channelState !== pondsocket_common_1.ChannelState.CLOSED) {
121
+ return channel;
122
+ }
123
+ const publisher = __classPrivateFieldGet(this, _SSEClient_instances, "m", _SSEClient_createPublisher).call(this);
124
+ const newChannel = new channel_1.Channel(publisher, this._connectionState, name, params || {});
125
+ __classPrivateFieldGet(this, _SSEClient_channels, "f").set(name, newChannel);
126
+ return newChannel;
127
+ }
128
+ onConnectionChange(callback) {
129
+ return this._connectionState.subscribe(callback);
130
+ }
131
+ onError(callback) {
132
+ return this._errorSubject.subscribe(callback);
133
+ }
134
+ }
135
+ exports.SSEClient = SSEClient;
136
+ _SSEClient_channels = new WeakMap(), _SSEClient_instances = new WeakSet(), _SSEClient_handleMessage = function _SSEClient_handleMessage(data) {
137
+ try {
138
+ const lines = data.trim().split('\n');
139
+ for (const line of lines) {
140
+ if (line.trim()) {
141
+ const parsed = JSON.parse(line);
142
+ const event = pondsocket_common_1.channelEventSchema.parse(parsed);
143
+ if (event.event === pondsocket_common_1.Events.CONNECTION && event.action === pondsocket_common_1.ServerActions.CONNECT) {
144
+ if (event.payload && typeof event.payload === 'object' && 'connectionId' in event.payload) {
145
+ this._connectionId = event.payload.connectionId;
146
+ }
147
+ }
148
+ this._broadcaster.publish(event);
149
+ }
150
+ }
151
+ }
152
+ catch (e) {
153
+ this._errorSubject.publish(e instanceof Error ? e : new Error('Failed to parse SSE message'));
154
+ }
155
+ }, _SSEClient_clearTimeout = function _SSEClient_clearTimeout() {
156
+ if (this._connectionTimeoutId) {
157
+ clearTimeout(this._connectionTimeoutId);
158
+ this._connectionTimeoutId = undefined;
159
+ }
160
+ }, _SSEClient_createPublisher = function _SSEClient_createPublisher() {
161
+ return (message) => {
162
+ if (this._connectionState.value === types_1.ConnectionState.CONNECTED && this._connectionId) {
163
+ fetch(this._postAddress.toString(), {
164
+ method: 'POST',
165
+ headers: {
166
+ 'Content-Type': 'application/json',
167
+ 'X-Connection-ID': this._connectionId,
168
+ },
169
+ body: JSON.stringify(message),
170
+ credentials: this._options.withCredentials ? 'include' : 'same-origin',
171
+ }).catch((err) => {
172
+ this._errorSubject.publish(err instanceof Error ? err : new Error('Failed to send message'));
173
+ });
174
+ }
175
+ };
176
+ }, _SSEClient_handleAcknowledge = function _SSEClient_handleAcknowledge(message) {
177
+ var _a;
178
+ const channel = (_a = __classPrivateFieldGet(this, _SSEClient_channels, "f").get(message.channelName)) !== null && _a !== void 0 ? _a : new channel_1.Channel(__classPrivateFieldGet(this, _SSEClient_instances, "m", _SSEClient_createPublisher).call(this), this._connectionState, message.channelName, {});
179
+ __classPrivateFieldGet(this, _SSEClient_channels, "f").set(message.channelName, channel);
180
+ channel.acknowledge(this._broadcaster);
181
+ }, _SSEClient_handleUnauthorized = function _SSEClient_handleUnauthorized(message) {
182
+ const channel = __classPrivateFieldGet(this, _SSEClient_channels, "f").get(message.channelName);
183
+ if (channel) {
184
+ const payload = message.payload;
185
+ channel.decline(payload);
186
+ }
187
+ }, _SSEClient_init = function _SSEClient_init() {
188
+ this._broadcaster.subscribe((message) => {
189
+ if (message.event === pondsocket_common_1.Events.ACKNOWLEDGE) {
190
+ __classPrivateFieldGet(this, _SSEClient_instances, "m", _SSEClient_handleAcknowledge).call(this, message);
191
+ }
192
+ else if (message.event === pondsocket_common_1.Events.UNAUTHORIZED) {
193
+ __classPrivateFieldGet(this, _SSEClient_instances, "m", _SSEClient_handleUnauthorized).call(this, message);
194
+ }
195
+ else if (message.event === pondsocket_common_1.Events.CONNECTION && message.action === pondsocket_common_1.ServerActions.CONNECT) {
196
+ this._connectionState.publish(types_1.ConnectionState.CONNECTED);
197
+ }
198
+ });
199
+ };
@@ -0,0 +1,202 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ const pondsocket_common_1 = require("@eleven-am/pondsocket-common");
13
+ const sseClient_1 = require("./sseClient");
14
+ const types_1 = require("../types");
15
+ class MockEventSource {
16
+ constructor(url, options) {
17
+ this.onopen = null;
18
+ this.onmessage = null;
19
+ this.onerror = null;
20
+ this.readyState = MockEventSource.CONNECTING;
21
+ this.close = jest.fn();
22
+ this.url = url;
23
+ this.options = options;
24
+ }
25
+ }
26
+ MockEventSource.CONNECTING = 0;
27
+ MockEventSource.OPEN = 1;
28
+ MockEventSource.CLOSED = 2;
29
+ // @ts-expect-error
30
+ global.EventSource = MockEventSource;
31
+ global.fetch = jest.fn(() => Promise.resolve({
32
+ ok: true,
33
+ status: 202,
34
+ }));
35
+ describe('SSEClient', () => {
36
+ let sseClient;
37
+ beforeEach(() => {
38
+ sseClient = new sseClient_1.SSEClient('http://example.com/events');
39
+ jest.clearAllMocks();
40
+ });
41
+ afterEach(() => {
42
+ jest.clearAllMocks();
43
+ });
44
+ test('connect method should set up EventSource events', () => {
45
+ sseClient.connect();
46
+ const mockEventSource = sseClient['_eventSource'];
47
+ expect(mockEventSource === null || mockEventSource === void 0 ? void 0 : mockEventSource.onmessage).toBeInstanceOf(Function);
48
+ expect(mockEventSource === null || mockEventSource === void 0 ? void 0 : mockEventSource.onerror).toBeInstanceOf(Function);
49
+ expect(mockEventSource === null || mockEventSource === void 0 ? void 0 : mockEventSource.onopen).toBeInstanceOf(Function);
50
+ });
51
+ test('connect should transition to CONNECTING state', () => {
52
+ const stateCallback = jest.fn();
53
+ sseClient.onConnectionChange(stateCallback);
54
+ stateCallback.mockClear();
55
+ sseClient.connect();
56
+ expect(stateCallback).toHaveBeenCalledWith(types_1.ConnectionState.CONNECTING);
57
+ });
58
+ test('it should publish messages received from the server', () => {
59
+ var _a;
60
+ sseClient.connect();
61
+ const mockEventSource = sseClient['_eventSource'];
62
+ const broadcasterSpy = jest.spyOn(sseClient['_broadcaster'], 'publish');
63
+ const event = {
64
+ data: JSON.stringify({
65
+ action: pondsocket_common_1.ServerActions.SYSTEM,
66
+ channelName: 'testChannel',
67
+ requestId: '123',
68
+ payload: {},
69
+ event: 'testEvent',
70
+ }),
71
+ };
72
+ (_a = mockEventSource === null || mockEventSource === void 0 ? void 0 : mockEventSource.onmessage) === null || _a === void 0 ? void 0 : _a.call(mockEventSource, event);
73
+ expect(broadcasterSpy).toHaveBeenCalledTimes(1);
74
+ expect(broadcasterSpy).toHaveBeenCalledWith({
75
+ action: pondsocket_common_1.ServerActions.SYSTEM,
76
+ channelName: 'testChannel',
77
+ requestId: '123',
78
+ payload: {},
79
+ event: 'testEvent',
80
+ });
81
+ });
82
+ test('should extract connectionId from CONNECTION event', () => {
83
+ var _a;
84
+ sseClient.connect();
85
+ const mockEventSource = sseClient['_eventSource'];
86
+ const connectEvent = {
87
+ event: pondsocket_common_1.Events.CONNECTION,
88
+ action: pondsocket_common_1.ServerActions.CONNECT,
89
+ channelName: 'GATEWAY',
90
+ requestId: '123',
91
+ payload: { connectionId: 'test-conn-id' },
92
+ };
93
+ (_a = mockEventSource === null || mockEventSource === void 0 ? void 0 : mockEventSource.onmessage) === null || _a === void 0 ? void 0 : _a.call(mockEventSource, { data: JSON.stringify(connectEvent) });
94
+ expect(sseClient.getConnectionId()).toBe('test-conn-id');
95
+ });
96
+ test('should transition to CONNECTED state on CONNECTION event', () => {
97
+ var _a;
98
+ const mockCallback = jest.fn();
99
+ sseClient.onConnectionChange(mockCallback);
100
+ mockCallback.mockClear();
101
+ sseClient.connect();
102
+ const mockEventSource = sseClient['_eventSource'];
103
+ const connectEvent = {
104
+ event: pondsocket_common_1.Events.CONNECTION,
105
+ action: pondsocket_common_1.ServerActions.CONNECT,
106
+ channelName: 'GATEWAY',
107
+ requestId: '123',
108
+ payload: { connectionId: 'test-conn-id' },
109
+ };
110
+ (_a = mockEventSource === null || mockEventSource === void 0 ? void 0 : mockEventSource.onmessage) === null || _a === void 0 ? void 0 : _a.call(mockEventSource, { data: JSON.stringify(connectEvent) });
111
+ expect(mockCallback).toHaveBeenCalledWith(types_1.ConnectionState.CONNECTED);
112
+ });
113
+ test('createChannel method should create a new channel', () => {
114
+ const channel = sseClient.createChannel('testChannel');
115
+ expect(channel).toBeDefined();
116
+ expect(sseClient.createChannel('testChannel')).toBe(channel);
117
+ });
118
+ test('getState method should return the current state', () => {
119
+ expect(sseClient.getState()).toBe(types_1.ConnectionState.DISCONNECTED);
120
+ sseClient['_connectionState'].publish(types_1.ConnectionState.CONNECTED);
121
+ expect(sseClient.getState()).toBe(types_1.ConnectionState.CONNECTED);
122
+ });
123
+ test('onError method should subscribe to error events', () => {
124
+ var _a;
125
+ const errorCallback = jest.fn();
126
+ sseClient.onError(errorCallback);
127
+ sseClient.connect();
128
+ const mockEventSource = sseClient['_eventSource'];
129
+ (_a = mockEventSource === null || mockEventSource === void 0 ? void 0 : mockEventSource.onerror) === null || _a === void 0 ? void 0 : _a.call(mockEventSource, new Event('error'));
130
+ expect(errorCallback).toHaveBeenCalledTimes(1);
131
+ expect(errorCallback).toHaveBeenCalledWith(expect.any(Error));
132
+ });
133
+ test('disconnect should close EventSource and send DELETE request', () => {
134
+ sseClient.connect();
135
+ const mockEventSource = sseClient['_eventSource'];
136
+ sseClient['_connectionId'] = 'test-conn-id';
137
+ sseClient.disconnect();
138
+ expect(mockEventSource === null || mockEventSource === void 0 ? void 0 : mockEventSource.close).toHaveBeenCalled();
139
+ expect(sseClient.getState()).toBe(types_1.ConnectionState.DISCONNECTED);
140
+ expect(global.fetch).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
141
+ method: 'DELETE',
142
+ headers: expect.objectContaining({
143
+ 'X-Connection-ID': 'test-conn-id',
144
+ }),
145
+ }));
146
+ });
147
+ test('should send messages via POST with connection ID', () => __awaiter(void 0, void 0, void 0, function* () {
148
+ sseClient.connect();
149
+ sseClient['_connectionId'] = 'test-conn-id';
150
+ sseClient['_connectionState'].publish(types_1.ConnectionState.CONNECTED);
151
+ const channel = sseClient.createChannel('testChannel');
152
+ const connectEvent = {
153
+ event: pondsocket_common_1.Events.ACKNOWLEDGE,
154
+ action: pondsocket_common_1.ServerActions.SYSTEM,
155
+ channelName: 'testChannel',
156
+ requestId: '123',
157
+ payload: {},
158
+ };
159
+ sseClient['_broadcaster'].publish(connectEvent);
160
+ channel.sendMessage('testEvent', { data: 'test' });
161
+ yield new Promise((resolve) => setTimeout(resolve, 10));
162
+ expect(global.fetch).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
163
+ method: 'POST',
164
+ headers: expect.objectContaining({
165
+ 'Content-Type': 'application/json',
166
+ 'X-Connection-ID': 'test-conn-id',
167
+ }),
168
+ body: expect.any(String),
169
+ }));
170
+ }));
171
+ test('connection timeout should trigger error', () => {
172
+ jest.useFakeTimers();
173
+ const clientWithTimeout = new sseClient_1.SSEClient('http://example.com/events', {}, { connectionTimeout: 5000 });
174
+ const errorCallback = jest.fn();
175
+ clientWithTimeout.onError(errorCallback);
176
+ clientWithTimeout.connect();
177
+ const mockEventSource = clientWithTimeout['_eventSource'];
178
+ mockEventSource.readyState = MockEventSource.CONNECTING;
179
+ jest.advanceTimersByTime(5000);
180
+ expect(errorCallback).toHaveBeenCalledWith(expect.objectContaining({ message: 'Connection timeout' }));
181
+ expect(mockEventSource.close).toHaveBeenCalled();
182
+ jest.useRealTimers();
183
+ });
184
+ test('should reconnect with exponential backoff on error', () => __awaiter(void 0, void 0, void 0, function* () {
185
+ var _a;
186
+ const connectSpy = jest.spyOn(sseClient, 'connect');
187
+ sseClient.connect();
188
+ const mockEventSource = sseClient['_eventSource'];
189
+ expect(connectSpy).toHaveBeenCalledTimes(1);
190
+ connectSpy.mockClear();
191
+ (_a = mockEventSource === null || mockEventSource === void 0 ? void 0 : mockEventSource.onerror) === null || _a === void 0 ? void 0 : _a.call(mockEventSource, new Event('error'));
192
+ yield new Promise((resolve) => setTimeout(resolve, 1100));
193
+ expect(connectSpy).toHaveBeenCalledTimes(1);
194
+ }));
195
+ test('should use withCredentials option when set', () => {
196
+ var _a;
197
+ const clientWithCredentials = new sseClient_1.SSEClient('http://example.com/events', {}, { withCredentials: true });
198
+ clientWithCredentials.connect();
199
+ const mockEventSource = clientWithCredentials['_eventSource'];
200
+ expect((_a = mockEventSource.options) === null || _a === void 0 ? void 0 : _a.withCredentials).toBe(true);
201
+ });
202
+ });
package/core/channel.js CHANGED
@@ -14,6 +14,7 @@ var _Channel_instances, _Channel_name, _Channel_queue, _Channel_presence, _Chann
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.Channel = void 0;
16
16
  const pondsocket_common_1 = require("@eleven-am/pondsocket-common");
17
+ const types_1 = require("../types");
17
18
  class Channel {
18
19
  constructor(publisher, clientState, name, params) {
19
20
  _Channel_instances.add(this);
@@ -59,6 +60,14 @@ class Channel {
59
60
  __classPrivateFieldGet(this, _Channel_instances, "m", _Channel_init).call(this, receiver);
60
61
  __classPrivateFieldGet(this, _Channel_instances, "m", _Channel_emptyQueue).call(this);
61
62
  }
63
+ /**
64
+ * @desc Marks the channel join as declined by the server.
65
+ * @param payload - The decline payload containing status code and message.
66
+ */
67
+ decline(payload) {
68
+ __classPrivateFieldGet(this, _Channel_joinState, "f").publish(pondsocket_common_1.ChannelState.DECLINED);
69
+ __classPrivateFieldSet(this, _Channel_queue, [], "f");
70
+ }
62
71
  /**
63
72
  * @desc Connects to the channel.
64
73
  */
@@ -74,12 +83,12 @@ class Channel {
74
83
  return;
75
84
  }
76
85
  __classPrivateFieldGet(this, _Channel_joinState, "f").publish(pondsocket_common_1.ChannelState.JOINING);
77
- if (__classPrivateFieldGet(this, _Channel_clientState, "f").value) {
86
+ if (__classPrivateFieldGet(this, _Channel_clientState, "f").value === types_1.ConnectionState.CONNECTED) {
78
87
  __classPrivateFieldGet(this, _Channel_publisher, "f").call(this, message);
79
88
  }
80
89
  else {
81
90
  const unsubscribe = __classPrivateFieldGet(this, _Channel_clientState, "f").subscribe((state) => {
82
- if (state) {
91
+ if (state === types_1.ConnectionState.CONNECTED) {
83
92
  unsubscribe();
84
93
  if (__classPrivateFieldGet(this, _Channel_joinState, "f").value === pondsocket_common_1.ChannelState.JOINING) {
85
94
  __classPrivateFieldGet(this, _Channel_publisher, "f").call(this, message);
@@ -227,7 +236,7 @@ _Channel_name = new WeakMap(), _Channel_queue = new WeakMap(), _Channel_presence
227
236
  }
228
237
  });
229
238
  const unsubStateChange = __classPrivateFieldGet(this, _Channel_clientState, "f").subscribe((state) => {
230
- if (state && __classPrivateFieldGet(this, _Channel_joinState, "f").value === pondsocket_common_1.ChannelState.STALLED) {
239
+ if (state === types_1.ConnectionState.CONNECTED && __classPrivateFieldGet(this, _Channel_joinState, "f").value === pondsocket_common_1.ChannelState.STALLED) {
231
240
  const message = {
232
241
  action: pondsocket_common_1.ClientActions.JOIN_CHANNEL,
233
242
  event: pondsocket_common_1.ClientActions.JOIN_CHANNEL,
@@ -237,7 +246,7 @@ _Channel_name = new WeakMap(), _Channel_queue = new WeakMap(), _Channel_presence
237
246
  };
238
247
  __classPrivateFieldGet(this, _Channel_publisher, "f").call(this, message);
239
248
  }
240
- else if (!state && __classPrivateFieldGet(this, _Channel_joinState, "f").value === pondsocket_common_1.ChannelState.JOINED) {
249
+ else if (state !== types_1.ConnectionState.CONNECTED && __classPrivateFieldGet(this, _Channel_joinState, "f").value === pondsocket_common_1.ChannelState.JOINED) {
241
250
  __classPrivateFieldGet(this, _Channel_joinState, "f").publish(pondsocket_common_1.ChannelState.STALLED);
242
251
  }
243
252
  });
@@ -11,9 +11,10 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  const pondsocket_common_1 = require("@eleven-am/pondsocket-common");
13
13
  const channel_1 = require("./channel");
14
+ const types_1 = require("../types");
14
15
  const createChannel = (params = {}) => {
15
16
  const publisher = jest.fn();
16
- const state = new pondsocket_common_1.BehaviorSubject(true);
17
+ const state = new pondsocket_common_1.BehaviorSubject(types_1.ConnectionState.CONNECTED);
17
18
  const receiver = new pondsocket_common_1.Subject();
18
19
  const channel = new channel_1.Channel(publisher, state, 'test', params);
19
20
  return {
@@ -29,12 +30,12 @@ describe('Channel', () => {
29
30
  // the channel should be in the idle state when it is created
30
31
  expect(channel.channelState).toBe(pondsocket_common_1.ChannelState.IDLE);
31
32
  // if the socket is not connected the channel should not post a join message
32
- state.publish(false);
33
+ state.publish(types_1.ConnectionState.DISCONNECTED);
33
34
  channel.join();
34
35
  expect(publisher).not.toHaveBeenCalled();
35
36
  expect(channel.channelState).toBe(pondsocket_common_1.ChannelState.JOINING);
36
37
  // once the socket is connected, the channel should attempt to join
37
- state.publish(true);
38
+ state.publish(types_1.ConnectionState.CONNECTED);
38
39
  expect(channel.channelState).toBe(pondsocket_common_1.ChannelState.JOINING);
39
40
  expect(publisher).toHaveBeenCalledWith(expect.objectContaining({
40
41
  action: pondsocket_common_1.ClientActions.JOIN_CHANNEL,
@@ -46,11 +47,11 @@ describe('Channel', () => {
46
47
  channel.acknowledge(receiver);
47
48
  expect(channel.channelState).toBe(pondsocket_common_1.ChannelState.JOINED);
48
49
  // if the socket is disconnected, the channel should be stalled
49
- state.publish(false);
50
+ state.publish(types_1.ConnectionState.DISCONNECTED);
50
51
  expect(channel.channelState).toBe(pondsocket_common_1.ChannelState.STALLED);
51
52
  // once the socket is reconnected, a join message should be sent
52
53
  publisher.mockClear();
53
- state.publish(true);
54
+ state.publish(types_1.ConnectionState.CONNECTED);
54
55
  expect(channel.channelState).toBe(pondsocket_common_1.ChannelState.STALLED);
55
56
  expect(publisher).toHaveBeenCalledWith(expect.objectContaining({
56
57
  action: pondsocket_common_1.ClientActions.JOIN_CHANNEL,
@@ -66,17 +67,17 @@ describe('Channel', () => {
66
67
  expect(channel.channelState).toBe(pondsocket_common_1.ChannelState.CLOSED);
67
68
  publisher.mockClear();
68
69
  // if the socket closes, while the channel is closed, the channel should not attempt to join when the socket reconnects
69
- state.publish(false);
70
- state.publish(true);
70
+ state.publish(types_1.ConnectionState.DISCONNECTED);
71
+ state.publish(types_1.ConnectionState.CONNECTED);
71
72
  expect(publisher).not.toHaveBeenCalled();
72
73
  // if the channel is closed, during a join process, the channel should not attempt to join when the socket reconnects
73
74
  const { channel: channel2, publisher: publisher2, state: state2 } = createChannel();
74
- state2.publish(false);
75
+ state2.publish(types_1.ConnectionState.DISCONNECTED);
75
76
  channel2.join();
76
77
  expect(publisher2).not.toHaveBeenCalled();
77
78
  expect(channel2.channelState).toBe(pondsocket_common_1.ChannelState.JOINING);
78
79
  channel2.leave();
79
- state2.publish(true);
80
+ state2.publish(types_1.ConnectionState.CONNECTED);
80
81
  expect(publisher2).not.toHaveBeenCalled();
81
82
  // if for some reason the server responds with an ack, the channel should still join, (The server is the source of truth)
82
83
  channel2.acknowledge(receiver);
@@ -91,7 +92,7 @@ describe('Channel', () => {
91
92
  expect(stateListener).toHaveBeenCalledWith(pondsocket_common_1.ChannelState.JOINING);
92
93
  channel.acknowledge(receiver);
93
94
  expect(stateListener).toHaveBeenCalledWith(pondsocket_common_1.ChannelState.JOINED);
94
- state.publish(false);
95
+ state.publish(types_1.ConnectionState.DISCONNECTED);
95
96
  expect(stateListener).toHaveBeenCalledWith(pondsocket_common_1.ChannelState.STALLED);
96
97
  channel.leave();
97
98
  expect(stateListener).toHaveBeenCalledWith(pondsocket_common_1.ChannelState.CLOSED);
@@ -118,8 +119,8 @@ describe('Channel', () => {
118
119
  }));
119
120
  publisher.mockClear();
120
121
  // if the channel stalls and rejoins the previous message should not be sent again
121
- state.publish(false);
122
- state.publish(true);
122
+ state.publish(types_1.ConnectionState.DISCONNECTED);
123
+ state.publish(types_1.ConnectionState.CONNECTED);
123
124
  // acknowledge the join
124
125
  channel.acknowledge(receiver);
125
126
  expect(publisher).toHaveBeenCalledTimes(1); // The join message
@@ -133,12 +134,12 @@ describe('Channel', () => {
133
134
  }));
134
135
  publisher.mockClear();
135
136
  // if a message is sent while the channel is stalled, it should be queued
136
- state.publish(false);
137
+ state.publish(types_1.ConnectionState.DISCONNECTED);
137
138
  channel.sendMessage('test', {
138
139
  test: 'test stalling',
139
140
  });
140
141
  expect(publisher).not.toHaveBeenCalled();
141
- state.publish(true);
142
+ state.publish(types_1.ConnectionState.CONNECTED);
142
143
  publisher.mockClear(); // The join message
143
144
  // acknowledge the join
144
145
  channel.acknowledge(receiver);
@@ -517,7 +518,7 @@ describe('Channel', () => {
517
518
  });
518
519
  expect(presenceListener).not.toHaveBeenCalled();
519
520
  // if the channel is stalled, the presence event should not be sent to the listener
520
- state.publish(false);
521
+ state.publish(types_1.ConnectionState.DISCONNECTED);
521
522
  expect(channel.channelState).toBe(pondsocket_common_1.ChannelState.STALLED);
522
523
  receiver.publish({
523
524
  requestId: 'test',
@@ -542,7 +543,7 @@ describe('Channel', () => {
542
543
  },
543
544
  });
544
545
  expect(presenceListener).not.toHaveBeenCalled();
545
- state.publish(true);
546
+ state.publish(types_1.ConnectionState.CONNECTED);
546
547
  expect(channel.channelState).toBe(pondsocket_common_1.ChannelState.STALLED);
547
548
  receiver.publish({
548
549
  requestId: 'test',
@@ -685,7 +686,7 @@ describe('Channel', () => {
685
686
  expect(specificMessageListener).not.toHaveBeenCalled();
686
687
  });
687
688
  it('should be able to wait for a message', () => __awaiter(void 0, void 0, void 0, function* () {
688
- const state = new pondsocket_common_1.BehaviorSubject(true);
689
+ const state = new pondsocket_common_1.BehaviorSubject(types_1.ConnectionState.CONNECTED);
689
690
  const receiver = new pondsocket_common_1.Subject();
690
691
  const params = {};
691
692
  const publisher = (data) => {
package/dist.d.ts CHANGED
@@ -10,6 +10,22 @@ import {
10
10
  Unsubscribe,
11
11
  } from '@eleven-am/pondsocket-common';
12
12
 
13
+ declare enum ConnectionState {
14
+ DISCONNECTED = 'DISCONNECTED',
15
+ CONNECTING = 'CONNECTING',
16
+ CONNECTED = 'CONNECTED',
17
+ }
18
+
19
+ interface ClientOptions {
20
+ connectionTimeout?: number;
21
+ maxReconnectDelay?: number;
22
+ pingInterval?: number;
23
+ }
24
+
25
+ interface SSEClientOptions extends ClientOptions {
26
+ withCredentials?: boolean;
27
+ }
28
+
13
29
  declare class Channel<EventMap extends PondEventMap = PondEventMap, Presence extends PondPresence = PondPresence> {
14
30
  /**
15
31
  * @desc The current connection state of the channel.
@@ -90,7 +106,7 @@ declare class Channel<EventMap extends PondEventMap = PondEventMap, Presence ext
90
106
  }
91
107
 
92
108
  declare class PondClient {
93
- constructor (endpoint: string, params?: Record<string, any>);
109
+ constructor (endpoint: string, params?: Record<string, any>, options?: ClientOptions);
94
110
 
95
111
  /**
96
112
  * @desc Connects to the server and returns the socket.
@@ -100,7 +116,7 @@ declare class PondClient {
100
116
  /**
101
117
  * @desc Returns the current state of the socket.
102
118
  */
103
- getState (): boolean;
119
+ getState (): ConnectionState;
104
120
 
105
121
  /**
106
122
  * @desc Disconnects the socket.
@@ -118,5 +134,54 @@ declare class PondClient {
118
134
  * @desc Subscribes to the connection state.
119
135
  * @param callback - The callback to call when the state changes.
120
136
  */
121
- onConnectionChange (callback: (state: boolean) => void): Unsubscribe;
137
+ onConnectionChange (callback: (state: ConnectionState) => void): Unsubscribe;
138
+
139
+ /**
140
+ * @desc Subscribes to error events.
141
+ * @param callback - The callback to call when an error occurs.
142
+ */
143
+ onError (callback: (error: Error) => void): Unsubscribe;
144
+ }
145
+
146
+ declare class SSEClient {
147
+ constructor (endpoint: string, params?: Record<string, any>, options?: SSEClientOptions);
148
+
149
+ /**
150
+ * @desc Connects to the server using Server-Sent Events.
151
+ */
152
+ connect (): void;
153
+
154
+ /**
155
+ * @desc Returns the current state of the connection.
156
+ */
157
+ getState (): ConnectionState;
158
+
159
+ /**
160
+ * @desc Returns the connection ID assigned by the server.
161
+ */
162
+ getConnectionId (): string | undefined;
163
+
164
+ /**
165
+ * @desc Disconnects the SSE connection.
166
+ */
167
+ disconnect (): void;
168
+
169
+ /**
170
+ * @desc Creates a channel with the given name and params.
171
+ * @param name - The name of the channel.
172
+ * @param params - The params to send to the server.
173
+ */
174
+ createChannel<EventType extends PondEventMap = PondEventMap, Presence extends PondPresence = PondPresence> (name: string, params?: JoinParams): Channel<EventType, Presence>;
175
+
176
+ /**
177
+ * @desc Subscribes to the connection state.
178
+ * @param callback - The callback to call when the state changes.
179
+ */
180
+ onConnectionChange (callback: (state: ConnectionState) => void): Unsubscribe;
181
+
182
+ /**
183
+ * @desc Subscribes to error events.
184
+ * @param callback - The callback to call when an error occurs.
185
+ */
186
+ onError (callback: (error: Error) => void): Unsubscribe;
122
187
  }
package/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { PondClient } from './dist';
1
+ import { PondClient, SSEClient, ConnectionState } from './dist';
2
2
  import { ChannelState } from '@eleven-am/pondsocket-common';
3
3
 
4
- export { ChannelState, PondClient };
4
+ export { ChannelState, ConnectionState, PondClient, SSEClient };
package/index.js CHANGED
@@ -1,9 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.PondClient = exports.ChannelState = void 0;
3
+ exports.SSEClient = exports.PondClient = exports.ConnectionState = exports.ChannelState = void 0;
4
4
  const pondsocket_common_1 = require("@eleven-am/pondsocket-common");
5
5
  Object.defineProperty(exports, "ChannelState", { enumerable: true, get: function () { return pondsocket_common_1.ChannelState; } });
6
6
  const client_1 = require("./browser/client");
7
+ const sseClient_1 = require("./browser/sseClient");
8
+ Object.defineProperty(exports, "SSEClient", { enumerable: true, get: function () { return sseClient_1.SSEClient; } });
7
9
  const node_1 = require("./node/node");
10
+ const types_1 = require("./types");
11
+ Object.defineProperty(exports, "ConnectionState", { enumerable: true, get: function () { return types_1.ConnectionState; } });
8
12
  const PondClient = typeof window === 'undefined' ? node_1.PondClient : client_1.PondClient;
9
13
  exports.PondClient = PondClient;
package/node/node.js CHANGED
@@ -1,18 +1,47 @@
1
1
  "use strict";
2
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
3
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
4
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
5
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
6
+ };
7
+ var _PondClient_instances, _PondClient_clearTimeouts;
2
8
  Object.defineProperty(exports, "__esModule", { value: true });
3
9
  exports.PondClient = void 0;
4
10
  const pondsocket_common_1 = require("@eleven-am/pondsocket-common");
5
11
  const client_1 = require("../browser/client");
12
+ const types_1 = require("../types");
6
13
  // eslint-disable-next-line @typescript-eslint/no-var-requires,@typescript-eslint/no-require-imports
7
14
  const WebSocket = require('websocket').w3cwebsocket;
8
15
  class PondClient extends client_1.PondClient {
16
+ constructor() {
17
+ super(...arguments);
18
+ _PondClient_instances.add(this);
19
+ }
9
20
  /**
10
21
  * @desc Connects to the server and returns the socket.
11
22
  */
12
- connect(backoff = 1) {
23
+ connect() {
13
24
  this._disconnecting = false;
25
+ this._connectionState.publish(types_1.ConnectionState.CONNECTING);
14
26
  const socket = new WebSocket(this._address.toString());
15
- socket.onopen = () => this._connectionState.publish(true);
27
+ this._connectionTimeoutId = setTimeout(() => {
28
+ if (socket.readyState === WebSocket.CONNECTING) {
29
+ const error = new Error('Connection timeout');
30
+ this._errorSubject.publish(error);
31
+ socket.close();
32
+ }
33
+ }, this._options.connectionTimeout);
34
+ socket.onopen = () => {
35
+ __classPrivateFieldGet(this, _PondClient_instances, "m", _PondClient_clearTimeouts).call(this);
36
+ this._reconnectAttempts = 0;
37
+ if (this._options.pingInterval) {
38
+ this._pingIntervalId = setInterval(() => {
39
+ if (socket.readyState === WebSocket.OPEN) {
40
+ socket.send(JSON.stringify({ action: 'ping' }));
41
+ }
42
+ }, this._options.pingInterval);
43
+ }
44
+ };
16
45
  socket.onmessage = (message) => {
17
46
  const lines = message.data.trim().split('\n');
18
47
  for (const line of lines) {
@@ -23,16 +52,34 @@ class PondClient extends client_1.PondClient {
23
52
  }
24
53
  }
25
54
  };
26
- socket.onerror = () => socket.close();
55
+ socket.onerror = () => {
56
+ const error = new Error('WebSocket error');
57
+ this._errorSubject.publish(error);
58
+ socket.close();
59
+ };
27
60
  socket.onclose = () => {
28
- this._connectionState.publish(false);
61
+ __classPrivateFieldGet(this, _PondClient_instances, "m", _PondClient_clearTimeouts).call(this);
62
+ this._connectionState.publish(types_1.ConnectionState.DISCONNECTED);
29
63
  if (this._disconnecting) {
30
64
  return;
31
65
  }
66
+ const delay = Math.min(1000 * Math.pow(2, this._reconnectAttempts), this._options.maxReconnectDelay);
67
+ this._reconnectAttempts++;
32
68
  setTimeout(() => {
33
69
  this.connect();
34
- }, 1000);
70
+ }, delay);
35
71
  };
72
+ this._socket = socket;
36
73
  }
37
74
  }
38
75
  exports.PondClient = PondClient;
76
+ _PondClient_instances = new WeakSet(), _PondClient_clearTimeouts = function _PondClient_clearTimeouts() {
77
+ if (this._connectionTimeoutId) {
78
+ clearTimeout(this._connectionTimeoutId);
79
+ this._connectionTimeoutId = undefined;
80
+ }
81
+ if (this._pingIntervalId) {
82
+ clearInterval(this._pingIntervalId);
83
+ this._pingIntervalId = undefined;
84
+ }
85
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eleven-am/pondsocket-client",
3
- "version": "0.0.30",
3
+ "version": "0.0.32",
4
4
  "description": "PondSocket is a fast simple socket server",
5
5
  "keywords": [
6
6
  "socket",
@@ -30,19 +30,19 @@
30
30
  "pipeline": "npm run test && npm run build && npm run push"
31
31
  },
32
32
  "dependencies": {
33
- "@eleven-am/pondsocket-common": "^0.0.32",
33
+ "@eleven-am/pondsocket-common": "^0.0.34",
34
34
  "websocket": "^1.0.35"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/jest": "^30.0.0",
38
38
  "@types/websocket": "^1.0.10",
39
- "@typescript-eslint/eslint-plugin": "^8.46.1",
39
+ "@typescript-eslint/eslint-plugin": "^8.49.0",
40
40
  "eslint-plugin-file-progress": "^3.0.2",
41
41
  "eslint-plugin-import": "^2.32.0",
42
42
  "jest": "^30.2.0",
43
- "prettier": "^3.6.2",
43
+ "prettier": "^3.7.4",
44
44
  "supertest": "^7.1.4",
45
- "ts-jest": "^29.4.5",
45
+ "ts-jest": "^29.4.6",
46
46
  "ts-loader": "^9.5.4",
47
47
  "ts-node": "^10.9.2",
48
48
  "typescript": "^5.9.3"
package/types.js CHANGED
@@ -1,2 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ConnectionState = void 0;
4
+ var ConnectionState;
5
+ (function (ConnectionState) {
6
+ ConnectionState["DISCONNECTED"] = "disconnected";
7
+ ConnectionState["CONNECTING"] = "connecting";
8
+ ConnectionState["CONNECTED"] = "connected";
9
+ })(ConnectionState || (exports.ConnectionState = ConnectionState = {}));