@eleven-am/pondsocket-client 0.0.29 → 0.0.31
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 +72 -13
- package/browser/client.test.js +69 -11
- package/browser/sseClient.js +190 -0
- package/browser/sseClient.test.js +202 -0
- package/core/channel.js +5 -4
- package/core/channel.test.js +18 -17
- package/dist.d.ts +68 -3
- package/index.d.ts +2 -2
- package/index.js +5 -1
- package/node/node.js +61 -7
- package/package.json +1 -1
- package/types.js +7 -0
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_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(
|
|
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,21 +56,52 @@ 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
|
-
const
|
|
51
|
-
const
|
|
52
|
-
|
|
80
|
+
const lines = message.data.trim().split('\n');
|
|
81
|
+
for (const line of lines) {
|
|
82
|
+
if (line.trim()) {
|
|
83
|
+
const data = JSON.parse(line);
|
|
84
|
+
const event = pondsocket_common_1.channelEventSchema.parse(data);
|
|
85
|
+
this._broadcaster.publish(event);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
socket.onerror = (event) => {
|
|
90
|
+
const error = new Error('WebSocket error');
|
|
91
|
+
this._errorSubject.publish(error);
|
|
92
|
+
socket.close();
|
|
53
93
|
};
|
|
54
|
-
socket.onerror = () => socket.close();
|
|
55
94
|
socket.onclose = () => {
|
|
56
|
-
this.
|
|
95
|
+
__classPrivateFieldGet(this, _PondClient_instances, "m", _PondClient_clearTimeouts).call(this);
|
|
96
|
+
this._connectionState.publish(types_1.ConnectionState.DISCONNECTED);
|
|
57
97
|
if (this._disconnecting) {
|
|
58
98
|
return;
|
|
59
99
|
}
|
|
100
|
+
const delay = Math.min(1000 * Math.pow(2, this._reconnectAttempts), this._options.maxReconnectDelay);
|
|
101
|
+
this._reconnectAttempts++;
|
|
60
102
|
setTimeout(() => {
|
|
61
103
|
this.connect();
|
|
62
|
-
},
|
|
104
|
+
}, delay);
|
|
63
105
|
};
|
|
64
106
|
this._socket = socket;
|
|
65
107
|
}
|
|
@@ -74,8 +116,9 @@ class PondClient {
|
|
|
74
116
|
*/
|
|
75
117
|
disconnect() {
|
|
76
118
|
var _a;
|
|
77
|
-
this.
|
|
119
|
+
__classPrivateFieldGet(this, _PondClient_instances, "m", _PondClient_clearTimeouts).call(this);
|
|
78
120
|
this._disconnecting = true;
|
|
121
|
+
this._connectionState.publish(types_1.ConnectionState.DISCONNECTED);
|
|
79
122
|
(_a = this._socket) === null || _a === void 0 ? void 0 : _a.close();
|
|
80
123
|
__classPrivateFieldGet(this, _PondClient_channels, "f").clear();
|
|
81
124
|
}
|
|
@@ -101,11 +144,27 @@ class PondClient {
|
|
|
101
144
|
onConnectionChange(callback) {
|
|
102
145
|
return this._connectionState.subscribe(callback);
|
|
103
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
|
+
}
|
|
104
154
|
}
|
|
105
155
|
exports.PondClient = PondClient;
|
|
106
|
-
_PondClient_channels = new WeakMap(), _PondClient_instances = new WeakSet(),
|
|
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() {
|
|
107
166
|
return (message) => {
|
|
108
|
-
if (this._connectionState.value) {
|
|
167
|
+
if (this._connectionState.value === types_1.ConnectionState.CONNECTED) {
|
|
109
168
|
this._socket.send(JSON.stringify(message));
|
|
110
169
|
}
|
|
111
170
|
};
|
|
@@ -120,7 +179,7 @@ _PondClient_channels = new WeakMap(), _PondClient_instances = new WeakSet(), _Po
|
|
|
120
179
|
__classPrivateFieldGet(this, _PondClient_instances, "m", _PondClient_handleAcknowledge).call(this, message);
|
|
121
180
|
}
|
|
122
181
|
else if (message.event === pondsocket_common_1.Events.CONNECTION && message.action === pondsocket_common_1.ServerActions.CONNECT) {
|
|
123
|
-
this._connectionState.publish(
|
|
182
|
+
this._connectionState.publish(types_1.ConnectionState.CONNECTED);
|
|
124
183
|
}
|
|
125
184
|
});
|
|
126
185
|
};
|
package/browser/client.test.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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
|
-
|
|
98
|
-
|
|
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(
|
|
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(
|
|
107
|
-
pondClient['_connectionState'].publish(
|
|
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(
|
|
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('
|
|
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,190 @@
|
|
|
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_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_init = function _SSEClient_init() {
|
|
182
|
+
this._broadcaster.subscribe((message) => {
|
|
183
|
+
if (message.event === pondsocket_common_1.Events.ACKNOWLEDGE) {
|
|
184
|
+
__classPrivateFieldGet(this, _SSEClient_instances, "m", _SSEClient_handleAcknowledge).call(this, message);
|
|
185
|
+
}
|
|
186
|
+
else if (message.event === pondsocket_common_1.Events.CONNECTION && message.action === pondsocket_common_1.ServerActions.CONNECT) {
|
|
187
|
+
this._connectionState.publish(types_1.ConnectionState.CONNECTED);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
};
|
|
@@ -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);
|
|
@@ -74,12 +75,12 @@ class Channel {
|
|
|
74
75
|
return;
|
|
75
76
|
}
|
|
76
77
|
__classPrivateFieldGet(this, _Channel_joinState, "f").publish(pondsocket_common_1.ChannelState.JOINING);
|
|
77
|
-
if (__classPrivateFieldGet(this, _Channel_clientState, "f").value) {
|
|
78
|
+
if (__classPrivateFieldGet(this, _Channel_clientState, "f").value === types_1.ConnectionState.CONNECTED) {
|
|
78
79
|
__classPrivateFieldGet(this, _Channel_publisher, "f").call(this, message);
|
|
79
80
|
}
|
|
80
81
|
else {
|
|
81
82
|
const unsubscribe = __classPrivateFieldGet(this, _Channel_clientState, "f").subscribe((state) => {
|
|
82
|
-
if (state) {
|
|
83
|
+
if (state === types_1.ConnectionState.CONNECTED) {
|
|
83
84
|
unsubscribe();
|
|
84
85
|
if (__classPrivateFieldGet(this, _Channel_joinState, "f").value === pondsocket_common_1.ChannelState.JOINING) {
|
|
85
86
|
__classPrivateFieldGet(this, _Channel_publisher, "f").call(this, message);
|
|
@@ -227,7 +228,7 @@ _Channel_name = new WeakMap(), _Channel_queue = new WeakMap(), _Channel_presence
|
|
|
227
228
|
}
|
|
228
229
|
});
|
|
229
230
|
const unsubStateChange = __classPrivateFieldGet(this, _Channel_clientState, "f").subscribe((state) => {
|
|
230
|
-
if (state && __classPrivateFieldGet(this, _Channel_joinState, "f").value === pondsocket_common_1.ChannelState.STALLED) {
|
|
231
|
+
if (state === types_1.ConnectionState.CONNECTED && __classPrivateFieldGet(this, _Channel_joinState, "f").value === pondsocket_common_1.ChannelState.STALLED) {
|
|
231
232
|
const message = {
|
|
232
233
|
action: pondsocket_common_1.ClientActions.JOIN_CHANNEL,
|
|
233
234
|
event: pondsocket_common_1.ClientActions.JOIN_CHANNEL,
|
|
@@ -237,7 +238,7 @@ _Channel_name = new WeakMap(), _Channel_queue = new WeakMap(), _Channel_presence
|
|
|
237
238
|
};
|
|
238
239
|
__classPrivateFieldGet(this, _Channel_publisher, "f").call(this, message);
|
|
239
240
|
}
|
|
240
|
-
else if (
|
|
241
|
+
else if (state !== types_1.ConnectionState.CONNECTED && __classPrivateFieldGet(this, _Channel_joinState, "f").value === pondsocket_common_1.ChannelState.JOINED) {
|
|
241
242
|
__classPrivateFieldGet(this, _Channel_joinState, "f").publish(pondsocket_common_1.ChannelState.STALLED);
|
|
242
243
|
}
|
|
243
244
|
});
|
package/core/channel.test.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
70
|
-
state.publish(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
122
|
-
state.publish(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 ():
|
|
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:
|
|
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,31 +1,85 @@
|
|
|
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;
|
|
10
|
+
const pondsocket_common_1 = require("@eleven-am/pondsocket-common");
|
|
4
11
|
const client_1 = require("../browser/client");
|
|
12
|
+
const types_1 = require("../types");
|
|
5
13
|
// eslint-disable-next-line @typescript-eslint/no-var-requires,@typescript-eslint/no-require-imports
|
|
6
14
|
const WebSocket = require('websocket').w3cwebsocket;
|
|
7
15
|
class PondClient extends client_1.PondClient {
|
|
16
|
+
constructor() {
|
|
17
|
+
super(...arguments);
|
|
18
|
+
_PondClient_instances.add(this);
|
|
19
|
+
}
|
|
8
20
|
/**
|
|
9
21
|
* @desc Connects to the server and returns the socket.
|
|
10
22
|
*/
|
|
11
|
-
connect(
|
|
23
|
+
connect() {
|
|
12
24
|
this._disconnecting = false;
|
|
25
|
+
this._connectionState.publish(types_1.ConnectionState.CONNECTING);
|
|
13
26
|
const socket = new WebSocket(this._address.toString());
|
|
14
|
-
|
|
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
|
+
};
|
|
15
45
|
socket.onmessage = (message) => {
|
|
16
|
-
const
|
|
17
|
-
|
|
46
|
+
const lines = message.data.trim().split('\n');
|
|
47
|
+
for (const line of lines) {
|
|
48
|
+
if (line.trim()) {
|
|
49
|
+
const data = JSON.parse(line);
|
|
50
|
+
const event = pondsocket_common_1.channelEventSchema.parse(data);
|
|
51
|
+
this._broadcaster.publish(event);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
socket.onerror = () => {
|
|
56
|
+
const error = new Error('WebSocket error');
|
|
57
|
+
this._errorSubject.publish(error);
|
|
58
|
+
socket.close();
|
|
18
59
|
};
|
|
19
|
-
socket.onerror = () => socket.close();
|
|
20
60
|
socket.onclose = () => {
|
|
21
|
-
this.
|
|
61
|
+
__classPrivateFieldGet(this, _PondClient_instances, "m", _PondClient_clearTimeouts).call(this);
|
|
62
|
+
this._connectionState.publish(types_1.ConnectionState.DISCONNECTED);
|
|
22
63
|
if (this._disconnecting) {
|
|
23
64
|
return;
|
|
24
65
|
}
|
|
66
|
+
const delay = Math.min(1000 * Math.pow(2, this._reconnectAttempts), this._options.maxReconnectDelay);
|
|
67
|
+
this._reconnectAttempts++;
|
|
25
68
|
setTimeout(() => {
|
|
26
69
|
this.connect();
|
|
27
|
-
},
|
|
70
|
+
}, delay);
|
|
28
71
|
};
|
|
72
|
+
this._socket = socket;
|
|
29
73
|
}
|
|
30
74
|
}
|
|
31
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
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 = {}));
|