stelar-time-real 3.2.1 → 3.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/client.d.ts +56 -61
- package/src/client.d.ts.map +1 -1
- package/src/client.js +461 -736
- package/src/client.ts +364 -905
- package/src/index.d.ts +88 -126
- package/src/index.d.ts.map +1 -1
- package/src/index.js +812 -1181
- package/src/index.ts +608 -1582
- package/src/logger.d.ts +11 -17
- package/src/logger.d.ts.map +1 -1
- package/src/logger.js +31 -90
- package/src/logger.ts +35 -106
- package/src/protocol.d.ts +20 -38
- package/src/protocol.d.ts.map +1 -1
- package/src/protocol.js +83 -161
- package/src/protocol.ts +79 -211
- package/src/websocket.d.ts +31 -47
- package/src/websocket.d.ts.map +1 -1
- package/src/websocket.js +161 -222
- package/src/websocket.ts +115 -278
package/src/client.js
CHANGED
|
@@ -1,19 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @stelar-time-real Client
|
|
3
|
-
*
|
|
4
|
-
* Dual-environment: Browser (native WebSocket) + Node.js (manual WS or TCP binary).
|
|
5
|
-
* No external dependencies.
|
|
6
|
-
*/
|
|
1
|
+
/** @stelar-time-real Client — Browser WS / Node WS / binary TCP */
|
|
7
2
|
import { FrameParser, encodeJsonFrame, encodeBinaryFrame, encodeAckReqFrame, encodePingFrame, encodePongFrame, encodeJoinFrame, encodeLeaveFrame, FRAME_JSON, FRAME_BINARY, FRAME_PING, FRAME_PONG, FRAME_ACK_RES, FRAME_CONNECT, validateEventName, DEFAULT_MAX_FRAME_SIZE, } from './protocol.js';
|
|
8
|
-
import { WSFrameParser, generateWSKey, createWSTextFrameMasked, createWSBinaryFrameMasked, createWSCloseFrameMasked, createWSPongFrameMasked, OP_TEXT, OP_BINARY, OP_CLOSE, OP_PING, OP_PONG, } from './websocket.js';
|
|
3
|
+
import { WSFrameParser, generateWSKey, createWSTextFrameMasked, createWSBinaryFrameMasked, createWSCloseFrameMasked, createWSPongFrameMasked, OP_TEXT, OP_BINARY, OP_CLOSE, OP_PING, OP_PONG, clientWantsCompression, } from './websocket.js';
|
|
9
4
|
import { Logger, NULL_LOGGER } from './logger.js';
|
|
10
5
|
const isNode = typeof process !== 'undefined' && process.versions?.node != null;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
let _net = null;
|
|
14
|
-
let _tls = null;
|
|
15
|
-
let _https = null;
|
|
16
|
-
async function loadNodeModules() {
|
|
6
|
+
let _http, _net, _tls, _https;
|
|
7
|
+
async function loadModules() {
|
|
17
8
|
if (!_http) {
|
|
18
9
|
_http = await import('http');
|
|
19
10
|
_net = await import('net');
|
|
@@ -21,693 +12,529 @@ async function loadNodeModules() {
|
|
|
21
12
|
_https = await import('https');
|
|
22
13
|
}
|
|
23
14
|
}
|
|
24
|
-
class
|
|
25
|
-
constructor(
|
|
26
|
-
this.
|
|
27
|
-
this.
|
|
28
|
-
}
|
|
29
|
-
push(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
15
|
+
class MsgQueue {
|
|
16
|
+
constructor(max = 100) {
|
|
17
|
+
this.max = max;
|
|
18
|
+
this.q = [];
|
|
19
|
+
}
|
|
20
|
+
push(m) { if (this.q.length >= this.max)
|
|
21
|
+
this.q.shift(); this.q.push(m); return true; }
|
|
22
|
+
drain() { const m = this.q; this.q = []; return m; }
|
|
23
|
+
get length() { return this.q.length; }
|
|
24
|
+
clear() { this.q = []; }
|
|
25
|
+
}
|
|
26
|
+
/** WS binary framing: [4B headerLen BE][header JSON][binary payload] — length-prefixed, not null-delimited */
|
|
27
|
+
function encodeWSBinary(event, data) {
|
|
28
|
+
const hdr = Buffer.from(JSON.stringify({ event }), 'utf8');
|
|
29
|
+
const payload = new Uint8Array(data);
|
|
30
|
+
const frame = Buffer.alloc(4 + hdr.length + payload.length);
|
|
31
|
+
frame.writeUInt32BE(hdr.length, 0);
|
|
32
|
+
hdr.copy(frame, 4);
|
|
33
|
+
frame.set(payload, 4 + hdr.length);
|
|
34
|
+
return frame;
|
|
35
|
+
}
|
|
36
|
+
function parseWSBinary(payload) {
|
|
37
|
+
if (payload.length < 4)
|
|
38
|
+
return null;
|
|
39
|
+
const hdrLen = payload.readUInt32BE(0);
|
|
40
|
+
if (hdrLen > payload.length - 4)
|
|
41
|
+
return null;
|
|
42
|
+
try {
|
|
43
|
+
const hdr = JSON.parse(payload.subarray(4, 4 + hdrLen).toString('utf8'));
|
|
44
|
+
const buf = payload.subarray(4 + hdrLen);
|
|
45
|
+
return { event: hdr.event, buffer: buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength) };
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return null;
|
|
46
49
|
}
|
|
47
50
|
}
|
|
48
51
|
class StelarClient {
|
|
49
|
-
constructor(urlOrPort = 'localhost:3000',
|
|
52
|
+
constructor(urlOrPort = 'localhost:3000', o = {}) {
|
|
50
53
|
this.events = new Map();
|
|
51
|
-
this.
|
|
54
|
+
this._wild = null;
|
|
52
55
|
this._acks = new Map();
|
|
53
56
|
this._state = 'disconnected';
|
|
54
|
-
this.
|
|
55
|
-
this.
|
|
56
|
-
this.
|
|
57
|
+
this._reconnAttempts = 0;
|
|
58
|
+
this._hb = null;
|
|
59
|
+
this._manualClose = false;
|
|
57
60
|
this.id = null;
|
|
58
|
-
this.
|
|
59
|
-
this.
|
|
60
|
-
this.
|
|
61
|
-
this.
|
|
62
|
-
this.
|
|
61
|
+
this._reconnTimer = null;
|
|
62
|
+
this._ackCounter = 0;
|
|
63
|
+
this._sent = 0;
|
|
64
|
+
this._recv = 0;
|
|
65
|
+
this._connTime = 0;
|
|
66
|
+
this._lastErr = null;
|
|
63
67
|
this._ws = null;
|
|
64
|
-
this.
|
|
68
|
+
this._nodeSock = null;
|
|
65
69
|
this._wsParser = null;
|
|
66
|
-
this.
|
|
70
|
+
this._tcpSock = null;
|
|
67
71
|
this._tcpParser = null;
|
|
68
|
-
|
|
72
|
+
this._compress = false;
|
|
73
|
+
this._serverCompress = false;
|
|
74
|
+
this._writePaused = false;
|
|
75
|
+
this._writeQueue = [];
|
|
76
|
+
if (typeof urlOrPort === 'number')
|
|
69
77
|
this.url = `ws://localhost:${urlOrPort}`;
|
|
70
|
-
|
|
71
|
-
else if (urlOrPort.includes('://')) {
|
|
78
|
+
else if (urlOrPort.includes('://'))
|
|
72
79
|
this.url = urlOrPort.startsWith('http') ? 'ws' + urlOrPort.slice(4) : urlOrPort;
|
|
73
|
-
|
|
74
|
-
else {
|
|
80
|
+
else
|
|
75
81
|
this.url = `ws://${urlOrPort}`;
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
maxFrameSize: options.maxFrameSize || DEFAULT_MAX_FRAME_SIZE,
|
|
87
|
-
messageQueueSize: options.messageQueueSize || 100,
|
|
88
|
-
logger: options.logger !== undefined ? options.logger : 'warn',
|
|
89
|
-
tls: options.tls || false,
|
|
90
|
-
rejectUnauthorized: options.rejectUnauthorized !== false,
|
|
91
|
-
headers: options.headers || {},
|
|
92
|
-
customReconnectDelay: options.customReconnectDelay,
|
|
93
|
-
hooks: options.hooks || {},
|
|
82
|
+
this.opts = {
|
|
83
|
+
reconnection: o.reconnection !== false, reconnectionAttempts: o.reconnectionAttempts || 10,
|
|
84
|
+
reconnectionDelay: o.reconnectionDelay || 1000, maxReconnectionDelay: o.maxReconnectionDelay || 30000,
|
|
85
|
+
heartbeatInterval: o.heartbeatInterval || 30000, ackTimeout: o.ackTimeout || 5000,
|
|
86
|
+
mode: o.mode || 'ws', maxPayloadSize: o.maxPayloadSize || 10 * 1024 * 1024,
|
|
87
|
+
maxFrameSize: o.maxFrameSize || DEFAULT_MAX_FRAME_SIZE, messageQueueSize: o.messageQueueSize || 100,
|
|
88
|
+
logger: o.logger !== undefined ? o.logger : 'warn', tls: o.tls || false,
|
|
89
|
+
rejectUnauthorized: o.rejectUnauthorized !== false, headers: o.headers || {},
|
|
90
|
+
compression: o.compression || false,
|
|
91
|
+
customReconnectDelay: o.customReconnectDelay, hooks: o.hooks || {},
|
|
94
92
|
};
|
|
95
|
-
this.
|
|
96
|
-
|
|
97
|
-
this.log = NULL_LOGGER;
|
|
98
|
-
}
|
|
99
|
-
else if (this.options.logger instanceof Logger) {
|
|
100
|
-
this.log = this.options.logger;
|
|
101
|
-
}
|
|
102
|
-
else {
|
|
103
|
-
this.log = new Logger({
|
|
104
|
-
level: this.options.logger || 'warn',
|
|
105
|
-
prefix: 'stelar:client',
|
|
106
|
-
});
|
|
107
|
-
}
|
|
93
|
+
this._mq = new MsgQueue(this.opts.messageQueueSize);
|
|
94
|
+
this.log = o.logger === false ? NULL_LOGGER : o.logger instanceof Logger ? o.logger : new Logger({ level: o.logger || 'warn', prefix: 'stelar:client' });
|
|
108
95
|
}
|
|
109
96
|
getState() { return this._state; }
|
|
110
97
|
getId() { return this.id; }
|
|
111
98
|
getUrl() { return this.url; }
|
|
112
|
-
getMessagesSent() { return this.
|
|
113
|
-
getMessagesReceived() { return this.
|
|
114
|
-
getLastError() { return this.
|
|
115
|
-
getQueueSize() { return this.
|
|
116
|
-
getConnectTime() { return this.
|
|
117
|
-
setUrl(
|
|
118
|
-
|
|
99
|
+
getMessagesSent() { return this._sent; }
|
|
100
|
+
getMessagesReceived() { return this._recv; }
|
|
101
|
+
getLastError() { return this._lastErr; }
|
|
102
|
+
getQueueSize() { return this._mq.length; }
|
|
103
|
+
getConnectTime() { return this._connTime; }
|
|
104
|
+
setUrl(u) { this.url = u; return this; }
|
|
105
|
+
updateOptions(o) {
|
|
106
|
+
for (const k of ['reconnection', 'reconnectionAttempts', 'reconnectionDelay', 'maxReconnectionDelay', 'heartbeatInterval', 'ackTimeout', 'maxPayloadSize', 'maxFrameSize', 'messageQueueSize', 'headers', 'compression'])
|
|
107
|
+
if (o[k] !== undefined)
|
|
108
|
+
this.opts[k] = o[k];
|
|
109
|
+
if (o.customReconnectDelay !== undefined)
|
|
110
|
+
this.opts.customReconnectDelay = o.customReconnectDelay;
|
|
111
|
+
if (o.hooks !== undefined)
|
|
112
|
+
this.opts.hooks = { ...this.opts.hooks, ...o.hooks };
|
|
119
113
|
return this;
|
|
120
114
|
}
|
|
121
|
-
/** Update client options at runtime. Changes take effect immediately. */
|
|
122
|
-
updateOptions(options) {
|
|
123
|
-
if (options.reconnection !== undefined)
|
|
124
|
-
this.options.reconnection = options.reconnection;
|
|
125
|
-
if (options.reconnectionAttempts !== undefined)
|
|
126
|
-
this.options.reconnectionAttempts = options.reconnectionAttempts;
|
|
127
|
-
if (options.reconnectionDelay !== undefined)
|
|
128
|
-
this.options.reconnectionDelay = options.reconnectionDelay;
|
|
129
|
-
if (options.maxReconnectionDelay !== undefined)
|
|
130
|
-
this.options.maxReconnectionDelay = options.maxReconnectionDelay;
|
|
131
|
-
if (options.heartbeatInterval !== undefined)
|
|
132
|
-
this.options.heartbeatInterval = options.heartbeatInterval;
|
|
133
|
-
if (options.ackTimeout !== undefined)
|
|
134
|
-
this.options.ackTimeout = options.ackTimeout;
|
|
135
|
-
if (options.maxPayloadSize !== undefined)
|
|
136
|
-
this.options.maxPayloadSize = options.maxPayloadSize;
|
|
137
|
-
if (options.maxFrameSize !== undefined)
|
|
138
|
-
this.options.maxFrameSize = options.maxFrameSize;
|
|
139
|
-
if (options.messageQueueSize !== undefined)
|
|
140
|
-
this.options.messageQueueSize = options.messageQueueSize;
|
|
141
|
-
if (options.headers !== undefined)
|
|
142
|
-
this.options.headers = options.headers;
|
|
143
|
-
if (options.customReconnectDelay !== undefined)
|
|
144
|
-
this.options.customReconnectDelay = options.customReconnectDelay;
|
|
145
|
-
if (options.hooks !== undefined)
|
|
146
|
-
this.options.hooks = { ...this.options.hooks, ...options.hooks };
|
|
147
|
-
return this;
|
|
148
|
-
}
|
|
149
|
-
/** Read-only snapshot of current client options. */
|
|
150
115
|
getOptions() {
|
|
151
116
|
return Object.freeze({
|
|
152
|
-
reconnection: this.
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
mode: this.options.mode,
|
|
159
|
-
maxPayloadSize: this.options.maxPayloadSize,
|
|
160
|
-
messageQueueSize: this.options.messageQueueSize,
|
|
161
|
-
hasCustomReconnectDelay: !!this.options.customReconnectDelay,
|
|
162
|
-
hooks: Object.keys(this.options.hooks),
|
|
117
|
+
reconnection: this.opts.reconnection, reconnectionAttempts: this.opts.reconnectionAttempts,
|
|
118
|
+
reconnectionDelay: this.opts.reconnectionDelay, maxReconnectionDelay: this.opts.maxReconnectionDelay,
|
|
119
|
+
heartbeatInterval: this.opts.heartbeatInterval, ackTimeout: this.opts.ackTimeout, mode: this.opts.mode,
|
|
120
|
+
maxPayloadSize: this.opts.maxPayloadSize, messageQueueSize: this.opts.messageQueueSize,
|
|
121
|
+
compression: this.opts.compression,
|
|
122
|
+
hasCustomReconnectDelay: !!this.opts.customReconnectDelay, hooks: Object.keys(this.opts.hooks),
|
|
163
123
|
});
|
|
164
124
|
}
|
|
165
|
-
on(
|
|
166
|
-
|
|
167
|
-
return this;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
173
|
-
return this;
|
|
174
|
-
}
|
|
175
|
-
once(event, handler) {
|
|
176
|
-
const wrapped = (data) => {
|
|
177
|
-
this.off(event, wrapped);
|
|
178
|
-
handler(data);
|
|
179
|
-
};
|
|
180
|
-
this.on(event, wrapped);
|
|
181
|
-
return this;
|
|
182
|
-
}
|
|
183
|
-
onAll(handler) {
|
|
184
|
-
this._wildcardHandler = handler;
|
|
185
|
-
return this;
|
|
186
|
-
}
|
|
187
|
-
onAck(name, handler) {
|
|
188
|
-
this._acks.set(name, { handler, timer: null });
|
|
189
|
-
return this;
|
|
190
|
-
}
|
|
191
|
-
removeAllListeners(event) {
|
|
192
|
-
if (event) {
|
|
193
|
-
this.events.delete(event);
|
|
194
|
-
}
|
|
195
|
-
else {
|
|
196
|
-
this.events.clear();
|
|
197
|
-
}
|
|
198
|
-
return this;
|
|
199
|
-
}
|
|
125
|
+
on(ev, h) { this.events.set(ev, h); return this; }
|
|
126
|
+
off(ev, h) { if (this.events.get(ev) === h)
|
|
127
|
+
this.events.delete(ev); return this; }
|
|
128
|
+
once(ev, h) { const w = (d) => { this.off(ev, w); h(d); }; this.on(ev, w); return this; }
|
|
129
|
+
onAll(h) { this._wild = h; return this; }
|
|
130
|
+
onAck(name, h) { this._acks.set(name, { handler: h, timer: null }); return this; }
|
|
131
|
+
removeAllListeners(ev) { ev ? this.events.delete(ev) : this.events.clear(); return this; }
|
|
200
132
|
emit(event, data, opts = {}) {
|
|
201
133
|
try {
|
|
202
134
|
if (event)
|
|
203
135
|
validateEventName(event);
|
|
204
136
|
}
|
|
205
137
|
catch {
|
|
206
|
-
this.log.warn('Invalid event
|
|
138
|
+
this.log.warn('Invalid event', { event });
|
|
207
139
|
return this;
|
|
208
140
|
}
|
|
209
|
-
if (this.
|
|
210
|
-
|
|
211
|
-
if (result === false)
|
|
212
|
-
return this;
|
|
213
|
-
}
|
|
141
|
+
if (this.opts.hooks.onBeforeEmit?.({ event, data }) === false)
|
|
142
|
+
return this;
|
|
214
143
|
try {
|
|
215
|
-
const
|
|
216
|
-
if (
|
|
217
|
-
this.log.warn('Payload
|
|
144
|
+
const s = JSON.stringify(data);
|
|
145
|
+
if (s.length > this.opts.maxPayloadSize) {
|
|
146
|
+
this.log.warn('Payload too large', { event });
|
|
218
147
|
return this;
|
|
219
148
|
}
|
|
220
149
|
}
|
|
221
150
|
catch {
|
|
222
|
-
this.log.warn('Failed to serialize data', { event });
|
|
223
151
|
return this;
|
|
224
152
|
}
|
|
225
153
|
if (this._state !== 'connected') {
|
|
226
|
-
if (this.
|
|
227
|
-
this.
|
|
228
|
-
this.
|
|
229
|
-
if (this.options.hooks.onMessageQueued) {
|
|
230
|
-
this.options.hooks.onMessageQueued({ event, data, queueSize: this._messageQueue.length });
|
|
231
|
-
}
|
|
154
|
+
if (this.opts.reconnection) {
|
|
155
|
+
this._mq.push({ event, data, opts, ts: Date.now() });
|
|
156
|
+
this.opts.hooks.onMessageQueued?.({ event, data, queueSize: this._mq.length });
|
|
232
157
|
}
|
|
233
158
|
return this;
|
|
234
159
|
}
|
|
235
160
|
try {
|
|
236
|
-
|
|
237
|
-
if (opts.
|
|
238
|
-
this.
|
|
239
|
-
|
|
161
|
+
const send = (wsPayload, tcpPayload) => {
|
|
162
|
+
if (this.opts.mode === 'tcp' && this._tcpSock && !this._tcpSock.destroyed)
|
|
163
|
+
this._writeTCP(tcpPayload());
|
|
164
|
+
else if (this._nodeSock && !this._nodeSock.destroyed)
|
|
165
|
+
this._writeNodeWS(wsPayload());
|
|
166
|
+
else if (this._ws && this._ws.readyState === WebSocket.OPEN)
|
|
167
|
+
this._ws.send(JSON.stringify({ event, data, ...(opts.ack ? { _ackName: opts.ack } : {}), ...(opts._correlationId ? { _correlationId: opts._correlationId } : {}) }));
|
|
240
168
|
else {
|
|
241
|
-
this.
|
|
169
|
+
this._mq.push({ event, data, opts, ts: Date.now() });
|
|
170
|
+
return;
|
|
242
171
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
if (opts.
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
250
|
-
else if (this._ws && this._ws.readyState === WebSocket.OPEN) {
|
|
251
|
-
const payload = { event, data };
|
|
252
|
-
if (opts.ack)
|
|
253
|
-
payload._ackName = opts.ack;
|
|
254
|
-
this._ws.send(JSON.stringify(payload));
|
|
172
|
+
this._sent++;
|
|
173
|
+
};
|
|
174
|
+
if (opts.ack) {
|
|
175
|
+
send(() => { const p = { event, data, _ackName: opts.ack }; if (opts._correlationId)
|
|
176
|
+
p._correlationId = opts._correlationId; return createWSTextFrameMasked(JSON.stringify(p), this._compress); }, () => { const d = { event, data }; if (opts._correlationId)
|
|
177
|
+
d._correlationId = opts._correlationId; return encodeAckReqFrame(opts.ack, d, this.opts.maxFrameSize); });
|
|
255
178
|
}
|
|
256
179
|
else {
|
|
257
|
-
|
|
258
|
-
|
|
180
|
+
send(() => { const p = { event, data }; if (opts._correlationId)
|
|
181
|
+
p._correlationId = opts._correlationId; return createWSTextFrameMasked(JSON.stringify(p), this._compress); }, () => encodeJsonFrame(event, data, this.opts.maxFrameSize));
|
|
259
182
|
}
|
|
260
|
-
this._messagesSent++;
|
|
261
183
|
}
|
|
262
|
-
catch (
|
|
263
|
-
this.log.error('Emit error', { event, error: String(
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
}
|
|
267
|
-
this._messageQueue.push({ event, data, opts, timestamp: Date.now() });
|
|
184
|
+
catch (e) {
|
|
185
|
+
this.log.error('Emit error', { event, error: String(e) });
|
|
186
|
+
this.opts.hooks.onError?.({ error: e instanceof Error ? e : new Error(String(e)), context: 'emit' });
|
|
187
|
+
this._mq.push({ event, data, opts, ts: Date.now() });
|
|
268
188
|
}
|
|
269
189
|
return this;
|
|
270
190
|
}
|
|
271
191
|
emitBinary(event, data) {
|
|
272
|
-
if (data.byteLength > this.
|
|
273
|
-
this.log.warn('Binary
|
|
192
|
+
if (data.byteLength > this.opts.maxPayloadSize) {
|
|
193
|
+
this.log.warn('Binary too large', { event });
|
|
274
194
|
return this;
|
|
275
195
|
}
|
|
276
|
-
if (this.
|
|
277
|
-
|
|
278
|
-
if (result === false)
|
|
279
|
-
return this;
|
|
280
|
-
}
|
|
196
|
+
if (this.opts.hooks.onBeforeEmit?.({ event, data }) === false)
|
|
197
|
+
return this;
|
|
281
198
|
if (this._state !== 'connected')
|
|
282
199
|
return this;
|
|
283
200
|
try {
|
|
284
|
-
|
|
285
|
-
|
|
201
|
+
const safeCopy = Buffer.from(new Uint8Array(data));
|
|
202
|
+
if (this.opts.mode === 'tcp' && this._tcpSock && !this._tcpSock.destroyed) {
|
|
203
|
+
this._writeTCP(encodeBinaryFrame(event, safeCopy, this.opts.maxFrameSize));
|
|
286
204
|
}
|
|
287
|
-
else if (this.
|
|
288
|
-
|
|
289
|
-
const headerBytes = Buffer.from(header, 'utf8');
|
|
290
|
-
const combined = Buffer.alloc(headerBytes.length + 1 + data.byteLength);
|
|
291
|
-
headerBytes.copy(combined, 0);
|
|
292
|
-
combined[headerBytes.length] = 0;
|
|
293
|
-
combined.set(new Uint8Array(data), headerBytes.length + 1);
|
|
294
|
-
this._nodeSocket.write(createWSBinaryFrameMasked(combined));
|
|
205
|
+
else if (this._nodeSock && !this._nodeSock.destroyed) {
|
|
206
|
+
this._writeNodeWS(createWSBinaryFrameMasked(encodeWSBinary(event, safeCopy)));
|
|
295
207
|
}
|
|
296
208
|
else if (this._ws && this._ws.readyState === WebSocket.OPEN) {
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
const combined = new Uint8Array(headerBytes.length + 1 + data.byteLength);
|
|
300
|
-
combined.set(headerBytes, 0);
|
|
301
|
-
combined[headerBytes.length] = 0;
|
|
302
|
-
combined.set(new Uint8Array(data), headerBytes.length + 1);
|
|
303
|
-
this._ws.send(combined);
|
|
209
|
+
const frame = encodeWSBinary(event, safeCopy);
|
|
210
|
+
this._ws.send(frame);
|
|
304
211
|
}
|
|
305
|
-
this.
|
|
212
|
+
this._sent++;
|
|
306
213
|
}
|
|
307
|
-
catch (
|
|
308
|
-
this.log.error('Binary emit error', { event, error: String(
|
|
309
|
-
if (this.options.hooks.onError) {
|
|
310
|
-
this.options.hooks.onError({ error: err instanceof Error ? err : new Error(String(err)), context: 'emitBinary' });
|
|
311
|
-
}
|
|
214
|
+
catch (e) {
|
|
215
|
+
this.log.error('Binary emit error', { event, error: String(e) });
|
|
312
216
|
}
|
|
313
217
|
return this;
|
|
314
218
|
}
|
|
315
|
-
sendFile(file)
|
|
316
|
-
|
|
317
|
-
}
|
|
318
|
-
sendImage(blob) {
|
|
319
|
-
return this.emitBinary('image', blob);
|
|
320
|
-
}
|
|
321
|
-
/** Send a request and wait for an ACK response. Rejects on timeout. */
|
|
219
|
+
sendFile(f) { return this.emitBinary('file', f); }
|
|
220
|
+
sendImage(b) { return this.emitBinary('image', b); }
|
|
322
221
|
request(event, data, ackName) {
|
|
323
222
|
return new Promise((resolve, reject) => {
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
const handler = (responseData) => {
|
|
330
|
-
clearTimeout(timeout);
|
|
331
|
-
this._acks.delete(ackName);
|
|
332
|
-
resolve(responseData);
|
|
333
|
-
};
|
|
334
|
-
this._acks.set(ackName, { handler, timer: timeout });
|
|
335
|
-
this.emit(event, data, { ack: ackName });
|
|
223
|
+
const corrId = `${ackName}#${++this._ackCounter}`;
|
|
224
|
+
const t = setTimeout(() => { this._acks.delete(corrId); reject(new Error(`ACK '${ackName}' timeout`)); }, this.opts.ackTimeout);
|
|
225
|
+
t.unref();
|
|
226
|
+
this._acks.set(corrId, { handler: (d) => { clearTimeout(t); this._acks.delete(corrId); resolve(d); }, timer: t });
|
|
227
|
+
this.emit(event, data, { ack: ackName, _correlationId: corrId });
|
|
336
228
|
});
|
|
337
229
|
}
|
|
338
230
|
joinRoom(room) {
|
|
339
|
-
if (this.
|
|
231
|
+
if (this.opts.mode === 'tcp' && this._tcpSock && !this._tcpSock.destroyed)
|
|
340
232
|
try {
|
|
341
|
-
this.
|
|
233
|
+
this._tcpSock.write(encodeJoinFrame(room, this.opts.maxFrameSize));
|
|
342
234
|
}
|
|
343
235
|
catch { }
|
|
344
|
-
|
|
345
|
-
else {
|
|
236
|
+
else
|
|
346
237
|
this.emit('join-room', room);
|
|
347
|
-
}
|
|
348
238
|
return this;
|
|
349
239
|
}
|
|
350
240
|
leaveRoom(room) {
|
|
351
|
-
if (this.
|
|
241
|
+
if (this.opts.mode === 'tcp' && this._tcpSock && !this._tcpSock.destroyed)
|
|
352
242
|
try {
|
|
353
|
-
this.
|
|
243
|
+
this._tcpSock.write(encodeLeaveFrame(room));
|
|
354
244
|
}
|
|
355
245
|
catch { }
|
|
356
|
-
|
|
357
|
-
else {
|
|
246
|
+
else
|
|
358
247
|
this.emit('leave-room', room);
|
|
248
|
+
return this;
|
|
249
|
+
}
|
|
250
|
+
connect(cb) {
|
|
251
|
+
if (this._state === 'connected' || this._state === 'connecting')
|
|
252
|
+
return this;
|
|
253
|
+
if (this._state === 'reconnecting' && this._reconnTimer) {
|
|
254
|
+
clearTimeout(this._reconnTimer);
|
|
255
|
+
this._reconnTimer = null;
|
|
256
|
+
}
|
|
257
|
+
this._manualClose = false;
|
|
258
|
+
this._setState('connecting');
|
|
259
|
+
if (this.opts.mode === 'tcp' && isNode)
|
|
260
|
+
this._connectTCP();
|
|
261
|
+
else if (isNode)
|
|
262
|
+
this._connectNodeWS();
|
|
263
|
+
else
|
|
264
|
+
this._connectBrowser();
|
|
265
|
+
if (cb) {
|
|
266
|
+
const check = setInterval(() => { if (this._state === 'connected') {
|
|
267
|
+
clearInterval(check);
|
|
268
|
+
cb();
|
|
269
|
+
} }, 50);
|
|
270
|
+
const safety = setTimeout(() => clearInterval(check), this.opts.ackTimeout);
|
|
271
|
+
safety.unref();
|
|
359
272
|
}
|
|
360
273
|
return this;
|
|
361
274
|
}
|
|
362
|
-
|
|
363
|
-
this.
|
|
364
|
-
this.
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
this._tcpSocket.write(encodePingFrame());
|
|
368
|
-
}
|
|
369
|
-
catch { }
|
|
275
|
+
disconnect() {
|
|
276
|
+
this._manualClose = true;
|
|
277
|
+
if (this._tcpSock && !this._tcpSock.destroyed)
|
|
278
|
+
try {
|
|
279
|
+
this._tcpSock.destroy();
|
|
370
280
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
catch { }
|
|
281
|
+
catch { }
|
|
282
|
+
if (this._nodeSock && !this._nodeSock.destroyed) {
|
|
283
|
+
try {
|
|
284
|
+
this._nodeSock.write(createWSCloseFrameMasked());
|
|
376
285
|
}
|
|
377
|
-
|
|
378
|
-
|
|
286
|
+
catch { }
|
|
287
|
+
try {
|
|
288
|
+
this._nodeSock.end();
|
|
379
289
|
}
|
|
380
|
-
|
|
381
|
-
if (this._hbTimer && typeof this._hbTimer === 'object' && 'unref' in this._hbTimer) {
|
|
382
|
-
this._hbTimer.unref();
|
|
290
|
+
catch { }
|
|
383
291
|
}
|
|
292
|
+
if (this._ws)
|
|
293
|
+
try {
|
|
294
|
+
this._ws.close();
|
|
295
|
+
}
|
|
296
|
+
catch { }
|
|
297
|
+
this._fullCleanup();
|
|
298
|
+
this._setState('disconnected');
|
|
299
|
+
return this;
|
|
384
300
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
301
|
+
isConnected() { return this._state === 'connected'; }
|
|
302
|
+
/* ── Private ── */
|
|
303
|
+
_setState(s) {
|
|
304
|
+
const prev = this._state;
|
|
305
|
+
this._state = s;
|
|
306
|
+
if (prev !== s) {
|
|
307
|
+
this.log.debug('State', { from: prev, to: s });
|
|
308
|
+
this.opts.hooks.onStateChange?.({ from: prev, to: s });
|
|
389
309
|
}
|
|
390
310
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
311
|
+
_startHB() {
|
|
312
|
+
this._stopHB();
|
|
313
|
+
this._hb = setInterval(() => {
|
|
314
|
+
if (this.opts.mode === 'tcp' && this._tcpSock && !this._tcpSock.destroyed)
|
|
315
|
+
try {
|
|
316
|
+
this._tcpSock.write(encodePingFrame());
|
|
317
|
+
}
|
|
318
|
+
catch { }
|
|
319
|
+
else if (this._nodeSock && !this._nodeSock.destroyed)
|
|
320
|
+
try {
|
|
321
|
+
this._nodeSock.write(createWSTextFrameMasked(JSON.stringify({ event: 'pong', data: Date.now() }), this._compress));
|
|
322
|
+
}
|
|
323
|
+
catch { }
|
|
324
|
+
else if (this._ws && this._ws.readyState === WebSocket.OPEN)
|
|
325
|
+
this._ws.send(JSON.stringify({ event: 'pong', data: Date.now() }));
|
|
326
|
+
}, this.opts.heartbeatInterval);
|
|
327
|
+
this._hb?.unref?.();
|
|
328
|
+
}
|
|
329
|
+
_stopHB() { if (this._hb) {
|
|
330
|
+
clearInterval(this._hb);
|
|
331
|
+
this._hb = null;
|
|
332
|
+
} }
|
|
333
|
+
_getDelay() {
|
|
334
|
+
const base = this.opts.reconnectionDelay, max = this.opts.maxReconnectionDelay;
|
|
335
|
+
const def = Math.min(base * Math.pow(1.5, this._reconnAttempts - 1), max);
|
|
336
|
+
const custom = this.opts.hooks.onReconnectDelay?.({ attempt: this._reconnAttempts, defaultDelay: def });
|
|
337
|
+
if (typeof custom === 'number')
|
|
338
|
+
return custom;
|
|
339
|
+
if (this.opts.customReconnectDelay)
|
|
340
|
+
return this.opts.customReconnectDelay(this._reconnAttempts, base, max);
|
|
341
|
+
return Math.floor(def + def * 0.2 * Math.random());
|
|
342
|
+
}
|
|
343
|
+
_drain() {
|
|
344
|
+
if (!this._mq.length)
|
|
409
345
|
return;
|
|
410
|
-
const
|
|
411
|
-
this.log.info('Draining
|
|
412
|
-
for (const
|
|
346
|
+
const msgs = this._mq.drain();
|
|
347
|
+
this.log.info('Draining queue', { count: msgs.length });
|
|
348
|
+
for (const m of msgs) {
|
|
413
349
|
try {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
350
|
+
const p = { event: m.event, data: m.data };
|
|
351
|
+
if (m.opts.ack)
|
|
352
|
+
p._ackName = m.opts.ack;
|
|
353
|
+
if (m.opts._correlationId)
|
|
354
|
+
p._correlationId = m.opts._correlationId;
|
|
355
|
+
if (this.opts.mode === 'tcp' && this._tcpSock && !this._tcpSock.destroyed) {
|
|
356
|
+
this._tcpSock.write(m.opts.ack ? encodeAckReqFrame(m.opts.ack, p, this.opts.maxFrameSize) : encodeJsonFrame(m.event, m.data, this.opts.maxFrameSize));
|
|
421
357
|
}
|
|
422
|
-
else if (this.
|
|
423
|
-
|
|
424
|
-
if (msg.opts.ack)
|
|
425
|
-
payload._ackName = msg.opts.ack;
|
|
426
|
-
this._nodeSocket.write(createWSTextFrameMasked(JSON.stringify(payload)));
|
|
358
|
+
else if (this._nodeSock && !this._nodeSock.destroyed) {
|
|
359
|
+
this._nodeSock.write(createWSTextFrameMasked(JSON.stringify(p), this._compress));
|
|
427
360
|
}
|
|
428
361
|
else if (this._ws && this._ws.readyState === WebSocket.OPEN) {
|
|
429
|
-
|
|
430
|
-
if (msg.opts.ack)
|
|
431
|
-
payload._ackName = msg.opts.ack;
|
|
432
|
-
this._ws.send(JSON.stringify(payload));
|
|
362
|
+
this._ws.send(JSON.stringify(p));
|
|
433
363
|
}
|
|
434
|
-
this.
|
|
364
|
+
this._sent++;
|
|
435
365
|
}
|
|
436
|
-
catch (
|
|
437
|
-
this.log.error('
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
if (this.options.hooks.onQueueDrained) {
|
|
441
|
-
this.options.hooks.onQueueDrained({ count: messages.length });
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
_setState(state) {
|
|
445
|
-
const prev = this._state;
|
|
446
|
-
this._state = state;
|
|
447
|
-
if (prev !== state) {
|
|
448
|
-
this.log.debug('State changed', { from: prev, to: state });
|
|
449
|
-
if (this.options.hooks.onStateChange) {
|
|
450
|
-
this.options.hooks.onStateChange({ from: prev, to: state });
|
|
366
|
+
catch (e) {
|
|
367
|
+
this.log.error('Drain error', { event: m.event, error: String(e) });
|
|
451
368
|
}
|
|
452
369
|
}
|
|
370
|
+
this.opts.hooks.onQueueDrained?.({ count: msgs.length });
|
|
453
371
|
}
|
|
454
|
-
_cleanupAcks() {
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
clearTimeout(entry.timer);
|
|
458
|
-
}
|
|
459
|
-
this._acks.clear();
|
|
460
|
-
}
|
|
372
|
+
_cleanupAcks() { for (const [, e] of this._acks)
|
|
373
|
+
if (e.timer)
|
|
374
|
+
clearTimeout(e.timer); this._acks.clear(); }
|
|
461
375
|
_fullCleanup() {
|
|
462
|
-
this.
|
|
376
|
+
this._stopHB();
|
|
463
377
|
this._cleanupAcks();
|
|
464
|
-
if (this.
|
|
465
|
-
clearTimeout(this.
|
|
466
|
-
this.
|
|
378
|
+
if (this._reconnTimer) {
|
|
379
|
+
clearTimeout(this._reconnTimer);
|
|
380
|
+
this._reconnTimer = null;
|
|
467
381
|
}
|
|
468
|
-
this.
|
|
382
|
+
this._nodeSock = null;
|
|
469
383
|
this._wsParser = null;
|
|
470
|
-
this.
|
|
384
|
+
this._tcpSock = null;
|
|
471
385
|
this._tcpParser = null;
|
|
472
386
|
this._ws = null;
|
|
387
|
+
this._writePaused = false;
|
|
388
|
+
this._writeQueue = [];
|
|
473
389
|
}
|
|
474
|
-
|
|
475
|
-
if (this.
|
|
476
|
-
return
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
}
|
|
482
|
-
this._isManualClose = false;
|
|
483
|
-
this._setState('connecting');
|
|
484
|
-
if (this.options.mode === 'tcp' && isNode) {
|
|
485
|
-
this._connectTCP();
|
|
486
|
-
}
|
|
487
|
-
else if (isNode) {
|
|
488
|
-
this._connectNodeWS();
|
|
489
|
-
}
|
|
490
|
-
else {
|
|
491
|
-
this._connectBrowser();
|
|
390
|
+
_tryReconnect(fn) {
|
|
391
|
+
if (this._manualClose || !this.opts.reconnection)
|
|
392
|
+
return;
|
|
393
|
+
if (this._reconnAttempts >= this.opts.reconnectionAttempts) {
|
|
394
|
+
this.log.warn('Max reconnect attempts');
|
|
395
|
+
this.events.get('reconnect_failed')?.(undefined);
|
|
396
|
+
return;
|
|
492
397
|
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
},
|
|
500
|
-
|
|
501
|
-
|
|
398
|
+
this._reconnAttempts++;
|
|
399
|
+
this._setState('reconnecting');
|
|
400
|
+
const delay = this._getDelay();
|
|
401
|
+
this.log.info('Reconnecting', { attempt: this._reconnAttempts, delay });
|
|
402
|
+
this.events.get('reconnecting')?.(this._reconnAttempts);
|
|
403
|
+
this._reconnTimer = setTimeout(() => { this._reconnTimer = null; if (!this._manualClose)
|
|
404
|
+
fn(); }, delay);
|
|
405
|
+
}
|
|
406
|
+
_onConnected() {
|
|
407
|
+
this._setState('connected');
|
|
408
|
+
this._reconnAttempts = 0;
|
|
409
|
+
this._connTime = Date.now();
|
|
410
|
+
this.events.get('connect')?.(undefined);
|
|
411
|
+
this._startHB();
|
|
412
|
+
this._drain();
|
|
413
|
+
}
|
|
414
|
+
/* ── Backpressure-aware writes ── */
|
|
415
|
+
_writeTCP(buf) {
|
|
416
|
+
if (!this._tcpSock || this._tcpSock.destroyed)
|
|
417
|
+
return;
|
|
418
|
+
if (this._writePaused) {
|
|
419
|
+
this._writeQueue.push(buf);
|
|
420
|
+
return;
|
|
502
421
|
}
|
|
503
|
-
|
|
422
|
+
const ok = this._tcpSock.write(buf);
|
|
423
|
+
if (!ok)
|
|
424
|
+
this._writePaused = true;
|
|
504
425
|
}
|
|
505
|
-
|
|
506
|
-
this.
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
catch { }
|
|
512
|
-
}
|
|
513
|
-
if (this._nodeSocket && !this._nodeSocket.destroyed) {
|
|
514
|
-
try {
|
|
515
|
-
this._nodeSocket.write(createWSCloseFrameMasked());
|
|
516
|
-
}
|
|
517
|
-
catch { }
|
|
518
|
-
try {
|
|
519
|
-
this._nodeSocket.end();
|
|
520
|
-
}
|
|
521
|
-
catch { }
|
|
426
|
+
_writeNodeWS(buf) {
|
|
427
|
+
if (!this._nodeSock || this._nodeSock.destroyed)
|
|
428
|
+
return;
|
|
429
|
+
if (this._writePaused) {
|
|
430
|
+
this._writeQueue.push(buf);
|
|
431
|
+
return;
|
|
522
432
|
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
433
|
+
const ok = this._nodeSock.write(buf);
|
|
434
|
+
if (!ok)
|
|
435
|
+
this._writePaused = true;
|
|
436
|
+
}
|
|
437
|
+
_flushQueue() {
|
|
438
|
+
this._writePaused = false;
|
|
439
|
+
while (this._writeQueue.length) {
|
|
440
|
+
const buf = this._writeQueue.shift();
|
|
441
|
+
const sock = this.opts.mode === 'tcp' ? this._tcpSock : this._nodeSock;
|
|
442
|
+
if (sock && !sock.destroyed) {
|
|
443
|
+
const ok = sock.write(buf);
|
|
444
|
+
if (!ok) {
|
|
445
|
+
this._writePaused = true;
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
526
448
|
}
|
|
527
|
-
catch { }
|
|
528
449
|
}
|
|
529
|
-
this._fullCleanup();
|
|
530
|
-
this._setState('disconnected');
|
|
531
|
-
return this;
|
|
532
|
-
}
|
|
533
|
-
isConnected() {
|
|
534
|
-
return this._state === 'connected';
|
|
535
450
|
}
|
|
451
|
+
/* ── Browser WS ── */
|
|
536
452
|
_connectBrowser() {
|
|
537
453
|
try {
|
|
538
454
|
const ws = new WebSocket(this.url);
|
|
539
455
|
ws.binaryType = 'arraybuffer';
|
|
540
|
-
ws.onopen = () =>
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
this.log.info('Browser WS connected');
|
|
545
|
-
const handler = this.events.get('connect');
|
|
546
|
-
if (handler)
|
|
547
|
-
handler(undefined);
|
|
548
|
-
this.startHeartbeat();
|
|
549
|
-
this._drainQueue();
|
|
550
|
-
};
|
|
551
|
-
ws.onmessage = (e) => {
|
|
552
|
-
this._messagesReceived++;
|
|
553
|
-
this._handleBrowserMessage(e);
|
|
554
|
-
};
|
|
555
|
-
ws.onclose = (e) => {
|
|
556
|
-
this._setState('disconnected');
|
|
557
|
-
this._fullCleanup();
|
|
558
|
-
const handler = this.events.get('disconnect');
|
|
559
|
-
if (handler)
|
|
560
|
-
handler({ code: e.code, reason: e.reason });
|
|
561
|
-
this._tryReconnect(() => this._connectBrowser());
|
|
562
|
-
};
|
|
563
|
-
ws.onerror = () => {
|
|
564
|
-
this._lastError = new Error('WebSocket error');
|
|
565
|
-
const handler = this.events.get('error');
|
|
566
|
-
if (handler)
|
|
567
|
-
handler(this._lastError);
|
|
568
|
-
if (this.options.hooks.onError) {
|
|
569
|
-
this.options.hooks.onError({ error: this._lastError, context: 'browser-ws' });
|
|
570
|
-
}
|
|
571
|
-
};
|
|
456
|
+
ws.onopen = () => this._onConnected();
|
|
457
|
+
ws.onmessage = (e) => { this._recv++; this._handleBrowserMsg(e); };
|
|
458
|
+
ws.onclose = (e) => { this._setState('disconnected'); this._fullCleanup(); this.events.get('disconnect')?.({ code: e.code, reason: e.reason }); this._tryReconnect(() => this._connectBrowser()); };
|
|
459
|
+
ws.onerror = () => { this._lastErr = new Error('WebSocket error'); this.events.get('error')?.(this._lastErr); this.opts.hooks.onError?.({ error: this._lastErr, context: 'browser-ws' }); };
|
|
572
460
|
this._ws = ws;
|
|
573
461
|
}
|
|
574
|
-
catch (
|
|
575
|
-
this.
|
|
462
|
+
catch (e) {
|
|
463
|
+
this._lastErr = e instanceof Error ? e : new Error(String(e));
|
|
576
464
|
this._setState('disconnected');
|
|
577
465
|
this._tryReconnect(() => this._connectBrowser());
|
|
578
466
|
}
|
|
579
467
|
}
|
|
580
|
-
|
|
468
|
+
_handleBrowserMsg(e) {
|
|
581
469
|
try {
|
|
582
470
|
if (e.data instanceof ArrayBuffer) {
|
|
583
|
-
const
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
if (view[i] === 0) {
|
|
587
|
-
headerEnd = i;
|
|
588
|
-
break;
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
if (headerEnd === -1)
|
|
471
|
+
const buf = Buffer.from(e.data);
|
|
472
|
+
const parsed = parseWSBinary(buf);
|
|
473
|
+
if (!parsed)
|
|
592
474
|
return;
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
if (this.options.hooks.onMessage) {
|
|
597
|
-
this.options.hooks.onMessage({ event: header.event, data: buffer, isBinary: true });
|
|
598
|
-
}
|
|
599
|
-
const handler = this.events.get(header.event);
|
|
600
|
-
if (handler)
|
|
601
|
-
handler(buffer);
|
|
602
|
-
if (this._wildcardHandler) {
|
|
603
|
-
this._wildcardHandler({ event: header.event, data: buffer, isBinary: true, buffer });
|
|
604
|
-
}
|
|
475
|
+
this.opts.hooks.onMessage?.({ event: parsed.event, data: parsed.buffer, isBinary: true });
|
|
476
|
+
this.events.get(parsed.event)?.(parsed.buffer);
|
|
477
|
+
this._wild?.({ event: parsed.event, data: parsed.buffer, isBinary: true, buffer: parsed.buffer });
|
|
605
478
|
return;
|
|
606
479
|
}
|
|
607
|
-
const msg = JSON.parse(e.data);
|
|
608
|
-
const { event, data, _isAck } = msg;
|
|
480
|
+
const msg = JSON.parse(e.data), { event, data, _isAck } = msg;
|
|
609
481
|
if (event === 'ping')
|
|
610
482
|
return;
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
483
|
+
this.opts.hooks.onMessage?.({ event, data, isBinary: false });
|
|
484
|
+
if (_isAck) {
|
|
485
|
+
const key = msg._correlationId || event;
|
|
486
|
+
if (this._acks.has(key)) {
|
|
487
|
+
const entry = this._acks.get(key);
|
|
488
|
+
if (entry.timer)
|
|
489
|
+
clearTimeout(entry.timer);
|
|
490
|
+
this._acks.delete(key);
|
|
491
|
+
entry.handler(data);
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
621
494
|
}
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
handler(data);
|
|
625
|
-
if (this._wildcardHandler)
|
|
626
|
-
this._wildcardHandler({ event, data });
|
|
495
|
+
this.events.get(event)?.(data);
|
|
496
|
+
this._wild?.({ event, data });
|
|
627
497
|
}
|
|
628
498
|
catch { }
|
|
629
499
|
}
|
|
500
|
+
/* ── Node WS ── */
|
|
630
501
|
async _connectNodeWS() {
|
|
631
502
|
try {
|
|
632
|
-
await
|
|
503
|
+
await loadModules();
|
|
633
504
|
if (!_http)
|
|
634
505
|
return;
|
|
635
|
-
const parsed = new URL(this.url);
|
|
636
|
-
const isSecure = parsed.protocol === 'wss:' || this.options.tls;
|
|
506
|
+
const parsed = new URL(this.url), secure = parsed.protocol === 'wss:' || this.opts.tls;
|
|
637
507
|
const key = generateWSKey();
|
|
638
|
-
const
|
|
639
|
-
|
|
640
|
-
'
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
this._nodeSocket = socket;
|
|
659
|
-
this._wsParser = new WSFrameParser(this.options.maxFrameSize);
|
|
660
|
-
if (head.length > 0) {
|
|
661
|
-
this._processNodeWSData(head);
|
|
662
|
-
}
|
|
663
|
-
socket.on('data', (data) => {
|
|
664
|
-
this._processNodeWSData(data);
|
|
665
|
-
});
|
|
666
|
-
socket.on('close', () => {
|
|
667
|
-
this._setState('disconnected');
|
|
668
|
-
this._fullCleanup();
|
|
669
|
-
const handler = this.events.get('disconnect');
|
|
670
|
-
if (handler)
|
|
671
|
-
handler(undefined);
|
|
672
|
-
this._tryReconnect(() => this._connectNodeWS());
|
|
673
|
-
});
|
|
674
|
-
socket.on('error', (err) => {
|
|
675
|
-
this._lastError = err;
|
|
676
|
-
const handler = this.events.get('error');
|
|
677
|
-
if (handler)
|
|
678
|
-
handler(err);
|
|
679
|
-
if (this.options.hooks.onError) {
|
|
680
|
-
this.options.hooks.onError({ error: err, context: 'node-ws' });
|
|
681
|
-
}
|
|
682
|
-
});
|
|
683
|
-
socket.on('drain', () => {
|
|
684
|
-
socket.resume();
|
|
685
|
-
});
|
|
686
|
-
this._setState('connected');
|
|
687
|
-
this._reconnectAttempts = 0;
|
|
688
|
-
this._connectTime = Date.now();
|
|
689
|
-
this.log.info('Node.js WS connected', { secure: isSecure });
|
|
690
|
-
const connectHandler = this.events.get('connect');
|
|
691
|
-
if (connectHandler)
|
|
692
|
-
connectHandler(undefined);
|
|
693
|
-
this.startHeartbeat();
|
|
694
|
-
this._drainQueue();
|
|
695
|
-
});
|
|
696
|
-
req.on('error', (err) => {
|
|
697
|
-
this._lastError = err;
|
|
698
|
-
const handler = this.events.get('error');
|
|
699
|
-
if (handler)
|
|
700
|
-
handler(err);
|
|
701
|
-
this._tryReconnect(() => this._connectNodeWS());
|
|
508
|
+
const hdrs = { Upgrade: 'websocket', Connection: 'Upgrade', 'Sec-WebSocket-Key': key, 'Sec-WebSocket-Version': '13', ...this.opts.headers };
|
|
509
|
+
if (this.opts.compression)
|
|
510
|
+
hdrs['Sec-WebSocket-Extensions'] = 'permessage-deflate; client_no_context_takeover; server_no_context_takeover';
|
|
511
|
+
const mod = secure && _https ? _https : _http;
|
|
512
|
+
const req = mod.request({ hostname: parsed.hostname, port: parseInt(parsed.port) || (secure ? 443 : 80), path: parsed.pathname + parsed.search, method: 'GET', headers: hdrs, rejectUnauthorized: this.opts.rejectUnauthorized });
|
|
513
|
+
req.setTimeout(this.opts.ackTimeout, () => req.destroy(new Error('Timeout')));
|
|
514
|
+
req.on('upgrade', (res, socket, head) => {
|
|
515
|
+
const extHeader = res.headers['sec-websocket-extensions'];
|
|
516
|
+
this._serverCompress = this.opts.compression && !!extHeader && clientWantsCompression(extHeader);
|
|
517
|
+
this._compress = this._serverCompress;
|
|
518
|
+
this._nodeSock = socket;
|
|
519
|
+
this._wsParser = new WSFrameParser(this.opts.maxFrameSize);
|
|
520
|
+
if (head.length > 0)
|
|
521
|
+
this._processNodeWS(head);
|
|
522
|
+
socket.on('data', (d) => this._processNodeWS(d));
|
|
523
|
+
socket.on('close', () => { this._setState('disconnected'); this._fullCleanup(); this.events.get('disconnect')?.(undefined); this._tryReconnect(() => this._connectNodeWS()); });
|
|
524
|
+
socket.on('error', (e) => { this._lastErr = e; this.events.get('error')?.(e); this.opts.hooks.onError?.({ error: e, context: 'node-ws' }); });
|
|
525
|
+
socket.on('drain', () => this._flushQueue());
|
|
526
|
+
this.log.info('Node WS connected', { secure, compressed: this._compress });
|
|
527
|
+
this._onConnected();
|
|
702
528
|
});
|
|
529
|
+
req.on('error', (e) => { this._lastErr = e; this.events.get('error')?.(e); this._tryReconnect(() => this._connectNodeWS()); });
|
|
703
530
|
req.end();
|
|
704
531
|
}
|
|
705
|
-
catch (
|
|
706
|
-
this.
|
|
532
|
+
catch (e) {
|
|
533
|
+
this._lastErr = e instanceof Error ? e : new Error(String(e));
|
|
707
534
|
this._tryReconnect(() => this._connectNodeWS());
|
|
708
535
|
}
|
|
709
536
|
}
|
|
710
|
-
|
|
537
|
+
_processNodeWS(data) {
|
|
711
538
|
if (!this._wsParser)
|
|
712
539
|
return;
|
|
713
540
|
let frames;
|
|
@@ -715,175 +542,106 @@ class StelarClient {
|
|
|
715
542
|
frames = this._wsParser.feed(data);
|
|
716
543
|
}
|
|
717
544
|
catch {
|
|
718
|
-
this.log.error('WS
|
|
545
|
+
this.log.error('WS parse error');
|
|
719
546
|
return;
|
|
720
547
|
}
|
|
721
|
-
for (const
|
|
722
|
-
this.
|
|
723
|
-
this.
|
|
548
|
+
for (const f of frames) {
|
|
549
|
+
this._recv++;
|
|
550
|
+
this._handleNodeFrame(f);
|
|
724
551
|
}
|
|
725
552
|
}
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
if (this._nodeSocket && !this._nodeSocket.destroyed) {
|
|
553
|
+
_handleNodeFrame(f) {
|
|
554
|
+
if (f.opcode === OP_PING) {
|
|
555
|
+
if (this._nodeSock && !this._nodeSock.destroyed)
|
|
730
556
|
try {
|
|
731
|
-
this.
|
|
557
|
+
this._nodeSock.write(createWSPongFrameMasked());
|
|
732
558
|
}
|
|
733
559
|
catch { }
|
|
734
|
-
}
|
|
735
560
|
return;
|
|
736
561
|
}
|
|
737
|
-
if (opcode === OP_PONG)
|
|
562
|
+
if (f.opcode === OP_PONG)
|
|
738
563
|
return;
|
|
739
|
-
if (opcode === OP_CLOSE) {
|
|
740
|
-
if (this.
|
|
564
|
+
if (f.opcode === OP_CLOSE) {
|
|
565
|
+
if (this._nodeSock && !this._nodeSock.destroyed)
|
|
741
566
|
try {
|
|
742
|
-
this.
|
|
567
|
+
this._nodeSock.end();
|
|
743
568
|
}
|
|
744
569
|
catch { }
|
|
745
|
-
}
|
|
746
570
|
return;
|
|
747
571
|
}
|
|
748
|
-
if (opcode === OP_TEXT) {
|
|
572
|
+
if (f.opcode === OP_TEXT) {
|
|
749
573
|
try {
|
|
750
|
-
const msg = JSON.parse(payload.toString('utf8'));
|
|
751
|
-
const { event, data, _isAck } = msg;
|
|
574
|
+
const msg = JSON.parse(f.payload.toString('utf8')), { event, data, _isAck } = msg;
|
|
752
575
|
if (event === 'ping')
|
|
753
576
|
return;
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
577
|
+
this.opts.hooks.onMessage?.({ event, data, isBinary: false });
|
|
578
|
+
if (_isAck) {
|
|
579
|
+
const key = msg._correlationId || event;
|
|
580
|
+
if (this._acks.has(key)) {
|
|
581
|
+
const e = this._acks.get(key);
|
|
582
|
+
if (e.timer)
|
|
583
|
+
clearTimeout(e.timer);
|
|
584
|
+
this._acks.delete(key);
|
|
585
|
+
e.handler(data);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
764
588
|
}
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
handler(data);
|
|
768
|
-
if (this._wildcardHandler)
|
|
769
|
-
this._wildcardHandler({ event, data });
|
|
589
|
+
this.events.get(event)?.(data);
|
|
590
|
+
this._wild?.({ event, data });
|
|
770
591
|
}
|
|
771
592
|
catch { }
|
|
772
593
|
return;
|
|
773
594
|
}
|
|
774
|
-
if (opcode === OP_BINARY) {
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
if (headerEnd === -1)
|
|
784
|
-
return;
|
|
785
|
-
const headerStr = payload.subarray(0, headerEnd).toString('utf8');
|
|
786
|
-
const header = JSON.parse(headerStr);
|
|
787
|
-
const buffer = payload.subarray(headerEnd + 1).buffer;
|
|
788
|
-
if (this.options.hooks.onMessage) {
|
|
789
|
-
this.options.hooks.onMessage({ event: header.event, data: buffer, isBinary: true });
|
|
790
|
-
}
|
|
791
|
-
const handler = this.events.get(header.event);
|
|
792
|
-
if (handler)
|
|
793
|
-
handler(buffer);
|
|
794
|
-
if (this._wildcardHandler)
|
|
795
|
-
this._wildcardHandler({ event: header.event, data: buffer, isBinary: true, buffer });
|
|
796
|
-
}
|
|
797
|
-
catch { }
|
|
595
|
+
if (f.opcode === OP_BINARY) {
|
|
596
|
+
const parsed = parseWSBinary(f.payload);
|
|
597
|
+
if (!parsed)
|
|
598
|
+
return;
|
|
599
|
+
this.opts.hooks.onMessage?.({ event: parsed.event, data: parsed.buffer, isBinary: true });
|
|
600
|
+
this.events.get(parsed.event)?.(parsed.buffer);
|
|
601
|
+
this._wild?.({ event: parsed.event, data: parsed.buffer, isBinary: true, buffer: parsed.buffer });
|
|
798
602
|
}
|
|
799
603
|
}
|
|
800
|
-
|
|
604
|
+
/* ── TCP ── */
|
|
801
605
|
async _connectTCP() {
|
|
802
606
|
try {
|
|
803
|
-
await
|
|
607
|
+
await loadModules();
|
|
804
608
|
if (!_net)
|
|
805
609
|
return;
|
|
806
|
-
const parsed = new URL(this.url);
|
|
807
|
-
const
|
|
808
|
-
const
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
if (this.
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
handler(undefined);
|
|
831
|
-
this.startHeartbeat();
|
|
832
|
-
this._drainQueue();
|
|
833
|
-
});
|
|
834
|
-
socket.on('data', (data) => {
|
|
835
|
-
if (!this._tcpParser)
|
|
836
|
-
return;
|
|
837
|
-
let frames;
|
|
838
|
-
try {
|
|
839
|
-
frames = this._tcpParser.feed(data);
|
|
840
|
-
}
|
|
841
|
-
catch {
|
|
842
|
-
this.log.error('TCP frame parse error');
|
|
843
|
-
socket.destroy();
|
|
844
|
-
return;
|
|
845
|
-
}
|
|
846
|
-
for (const frame of frames) {
|
|
847
|
-
this._messagesReceived++;
|
|
848
|
-
this._handleTCPFrame(frame);
|
|
849
|
-
}
|
|
850
|
-
});
|
|
851
|
-
socket.on('close', () => {
|
|
852
|
-
this._setState('disconnected');
|
|
853
|
-
this._fullCleanup();
|
|
854
|
-
const handler = this.events.get('disconnect');
|
|
855
|
-
if (handler)
|
|
856
|
-
handler(undefined);
|
|
857
|
-
this._tryReconnect(() => this._connectTCP());
|
|
858
|
-
});
|
|
859
|
-
socket.on('error', (err) => {
|
|
860
|
-
this._lastError = err;
|
|
861
|
-
const handler = this.events.get('error');
|
|
862
|
-
if (handler)
|
|
863
|
-
handler(err);
|
|
864
|
-
if (this.options.hooks.onError) {
|
|
865
|
-
this.options.hooks.onError({ error: err, context: 'tcp' });
|
|
866
|
-
}
|
|
867
|
-
});
|
|
868
|
-
socket.on('drain', () => {
|
|
869
|
-
socket.resume();
|
|
870
|
-
});
|
|
871
|
-
this._tcpSocket = socket;
|
|
872
|
-
}
|
|
873
|
-
catch (err) {
|
|
874
|
-
this._lastError = err instanceof Error ? err : new Error(String(err));
|
|
610
|
+
const parsed = new URL(this.url), port = parseInt(parsed.port) + 1 || 3001, host = parsed.hostname || 'localhost';
|
|
611
|
+
const sockOpts = { host, port };
|
|
612
|
+
const socket = this.opts.tls && _tls ? _tls.connect({ ...sockOpts, rejectUnauthorized: this.opts.rejectUnauthorized }) : _net.createConnection(sockOpts);
|
|
613
|
+
socket.setTimeout(this.opts.ackTimeout, () => socket.destroy(new Error('TCP timeout')));
|
|
614
|
+
socket.on('connect', () => { socket.setTimeout(0); this._tcpParser = new FrameParser(this.opts.maxFrameSize); this.log.info('TCP connected', { host, port }); this._onConnected(); });
|
|
615
|
+
socket.on('data', (d) => { if (!this._tcpParser)
|
|
616
|
+
return; let frames; try {
|
|
617
|
+
frames = this._tcpParser.feed(d);
|
|
618
|
+
}
|
|
619
|
+
catch {
|
|
620
|
+
this.log.error('TCP parse error');
|
|
621
|
+
socket.destroy();
|
|
622
|
+
return;
|
|
623
|
+
} for (const f of frames) {
|
|
624
|
+
this._recv++;
|
|
625
|
+
this._handleTCPFrame(f);
|
|
626
|
+
} });
|
|
627
|
+
socket.on('close', () => { this._setState('disconnected'); this._fullCleanup(); this.events.get('disconnect')?.(undefined); this._tryReconnect(() => this._connectTCP()); });
|
|
628
|
+
socket.on('error', (e) => { this._lastErr = e; this.events.get('error')?.(e); this.opts.hooks.onError?.({ error: e, context: 'tcp' }); });
|
|
629
|
+
socket.on('drain', () => this._flushQueue());
|
|
630
|
+
this._tcpSock = socket;
|
|
631
|
+
}
|
|
632
|
+
catch (e) {
|
|
633
|
+
this._lastErr = e instanceof Error ? e : new Error(String(e));
|
|
875
634
|
this._tryReconnect(() => this._connectTCP());
|
|
876
635
|
}
|
|
877
636
|
}
|
|
878
|
-
_handleTCPFrame(
|
|
879
|
-
const { type, event, payload } =
|
|
637
|
+
_handleTCPFrame(f) {
|
|
638
|
+
const { type, event, payload } = f;
|
|
880
639
|
if (type === FRAME_PING) {
|
|
881
|
-
if (this.
|
|
640
|
+
if (this._tcpSock && !this._tcpSock.destroyed)
|
|
882
641
|
try {
|
|
883
|
-
this.
|
|
642
|
+
this._tcpSock.write(encodePongFrame());
|
|
884
643
|
}
|
|
885
644
|
catch { }
|
|
886
|
-
}
|
|
887
645
|
return;
|
|
888
646
|
}
|
|
889
647
|
if (type === FRAME_PONG)
|
|
@@ -893,72 +651,39 @@ class StelarClient {
|
|
|
893
651
|
return;
|
|
894
652
|
}
|
|
895
653
|
if (type === FRAME_ACK_RES) {
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
this._acks.
|
|
903
|
-
|
|
654
|
+
try {
|
|
655
|
+
const raw = JSON.parse(payload.toString('utf8'));
|
|
656
|
+
const data = raw && typeof raw === 'object' && 'data' in raw ? raw.data : raw;
|
|
657
|
+
const corrId = raw && typeof raw === 'object' && '_correlationId' in raw ? String(raw._correlationId) : undefined;
|
|
658
|
+
const key = corrId || event;
|
|
659
|
+
if (this._acks.has(key)) {
|
|
660
|
+
const e = this._acks.get(key);
|
|
661
|
+
if (e.timer)
|
|
662
|
+
clearTimeout(e.timer);
|
|
663
|
+
this._acks.delete(key);
|
|
664
|
+
e.handler(data);
|
|
904
665
|
}
|
|
905
|
-
catch { }
|
|
906
666
|
}
|
|
667
|
+
catch { }
|
|
907
668
|
return;
|
|
908
669
|
}
|
|
909
670
|
if (type === FRAME_JSON) {
|
|
910
671
|
try {
|
|
911
672
|
const data = JSON.parse(payload.toString('utf8'));
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
}
|
|
915
|
-
const handler = this.events.get(event);
|
|
916
|
-
if (handler)
|
|
917
|
-
handler(data);
|
|
918
|
-
if (this._wildcardHandler)
|
|
919
|
-
this._wildcardHandler({ event, data });
|
|
673
|
+
this.opts.hooks.onMessage?.({ event, data, isBinary: false });
|
|
674
|
+
this.events.get(event)?.(data);
|
|
675
|
+
this._wild?.({ event, data });
|
|
920
676
|
}
|
|
921
677
|
catch { }
|
|
922
678
|
return;
|
|
923
679
|
}
|
|
924
680
|
if (type === FRAME_BINARY) {
|
|
925
681
|
const copy = Buffer.from(payload);
|
|
926
|
-
const
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
}
|
|
930
|
-
const handler = this.events.get(event);
|
|
931
|
-
if (handler)
|
|
932
|
-
handler(buffer);
|
|
933
|
-
if (this._wildcardHandler)
|
|
934
|
-
this._wildcardHandler({ event, data: buffer, isBinary: true, buffer });
|
|
682
|
+
const buf = copy.buffer.slice(copy.byteOffset, copy.byteOffset + copy.byteLength);
|
|
683
|
+
this.opts.hooks.onMessage?.({ event, data: buf, isBinary: true });
|
|
684
|
+
this.events.get(event)?.(buf);
|
|
685
|
+
this._wild?.({ event, data: buf, isBinary: true, buffer: buf });
|
|
935
686
|
}
|
|
936
687
|
}
|
|
937
|
-
_tryReconnect(connectFn) {
|
|
938
|
-
if (this._isManualClose)
|
|
939
|
-
return;
|
|
940
|
-
if (!this.options.reconnection)
|
|
941
|
-
return;
|
|
942
|
-
if (this._reconnectAttempts >= this.options.reconnectionAttempts) {
|
|
943
|
-
this.log.warn('Max reconnection attempts reached', { attempts: this._reconnectAttempts });
|
|
944
|
-
const handler = this.events.get('reconnect_failed');
|
|
945
|
-
if (handler)
|
|
946
|
-
handler(undefined);
|
|
947
|
-
return;
|
|
948
|
-
}
|
|
949
|
-
this._reconnectAttempts++;
|
|
950
|
-
this._setState('reconnecting');
|
|
951
|
-
const delay = this._getReconnectDelay();
|
|
952
|
-
this.log.info('Reconnecting', { attempt: this._reconnectAttempts, delay });
|
|
953
|
-
const handler = this.events.get('reconnecting');
|
|
954
|
-
if (handler)
|
|
955
|
-
handler(this._reconnectAttempts);
|
|
956
|
-
this._reconnectTimer = setTimeout(() => {
|
|
957
|
-
this._reconnectTimer = null;
|
|
958
|
-
if (!this._isManualClose) {
|
|
959
|
-
connectFn();
|
|
960
|
-
}
|
|
961
|
-
}, delay);
|
|
962
|
-
}
|
|
963
688
|
}
|
|
964
689
|
export default StelarClient;
|