@culpeo/async-ws 0.2.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -1
- package/README.md +100 -2
- package/dist/browser/index.js +161 -15
- package/dist/cjs/index.cjs +161 -14
- package/dist/esm/index.js +161 -14
- package/dist/iife/index.js +161 -15
- package/dist/index.d.ts +43 -1
- package/package.json +8 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- 6b68e4c: ### Connect timeout and abort signal
|
|
8
|
+
|
|
9
|
+
`connect()` now accepts `timeout` (milliseconds) and `signal` (AbortSignal) options to cancel connection attempts.
|
|
10
|
+
|
|
11
|
+
### Keep-alive ping/pong (Node.js)
|
|
12
|
+
|
|
13
|
+
New `keepAlive` constructor option sends periodic pings and terminates the connection if no pong is received. Not available in browsers.
|
|
14
|
+
|
|
15
|
+
### Exposed WebSocket properties
|
|
16
|
+
|
|
17
|
+
Added read-only `protocol`, `url`, `bufferedAmount`, and `extensions` properties that delegate to the underlying socket.
|
|
18
|
+
|
|
3
19
|
## 0.2.0
|
|
4
20
|
|
|
5
21
|
### Minor Changes
|
|
6
22
|
|
|
7
23
|
- 9453a7c: Initial release
|
|
8
|
-
|
|
9
24
|
- Cross-platform WebSocket client for Node.js and browsers
|
|
10
25
|
- Promise-based `connect()`, `send()`, `receive()`, `close()` API
|
|
11
26
|
- Async iteration with `for await...of`
|
package/README.md
CHANGED
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
- Async iteration support with `for await...of`
|
|
16
16
|
- Message buffering for messages that arrive before `receive()` is called
|
|
17
17
|
- Configurable `maxBufferSize` with oldest-message eviction when full
|
|
18
|
+
- Connect timeout and `AbortSignal` support
|
|
19
|
+
- Keep-alive with automatic ping/pong (Node.js)
|
|
20
|
+
- Exposed WebSocket properties (`protocol`, `url`, `bufferedAmount`, `extensions`)
|
|
18
21
|
- Clean close information via `lastCloseInfo`
|
|
19
22
|
- TypeScript-first with bundled type definitions
|
|
20
23
|
- Binary and text message support
|
|
@@ -45,7 +48,7 @@ await client.connect("wss://echo.websocket.events");
|
|
|
45
48
|
await client.send("hello");
|
|
46
49
|
|
|
47
50
|
const message = await client.receive();
|
|
48
|
-
console.log(message.data);
|
|
51
|
+
console.log(message.data); // string | ArrayBuffer
|
|
49
52
|
console.log(message.binary); // boolean
|
|
50
53
|
|
|
51
54
|
await client.close();
|
|
@@ -69,6 +72,9 @@ Creates a new client instance.
|
|
|
69
72
|
- Maximum number of incoming messages to keep buffered before they are consumed
|
|
70
73
|
- Default: `0` (unlimited)
|
|
71
74
|
- When the limit is reached, the oldest buffered message is dropped
|
|
75
|
+
- `keepAlive?: KeepAliveOptions`
|
|
76
|
+
- Enables automatic ping/pong keep-alive (Node.js only)
|
|
77
|
+
- Throws if used in a browser environment
|
|
72
78
|
|
|
73
79
|
#### Properties
|
|
74
80
|
|
|
@@ -87,6 +93,38 @@ Returns the current client state:
|
|
|
87
93
|
- `"closed"`
|
|
88
94
|
- `"errored"`
|
|
89
95
|
|
|
96
|
+
#### `client.protocol`
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
readonly protocol: string
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Returns the negotiated subprotocol, or `""` when not connected.
|
|
103
|
+
|
|
104
|
+
#### `client.url`
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
readonly url: string
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Returns the URL of the WebSocket connection, or `""` when not connected.
|
|
111
|
+
|
|
112
|
+
#### `client.bufferedAmount`
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
readonly bufferedAmount: number
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Returns the number of bytes queued for transmission, or `0` when not connected.
|
|
119
|
+
|
|
120
|
+
#### `client.extensions`
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
readonly extensions: string
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Returns the negotiated extensions, or `""` when not connected.
|
|
127
|
+
|
|
90
128
|
#### `client.lastCloseInfo`
|
|
91
129
|
|
|
92
130
|
```ts
|
|
@@ -116,6 +154,8 @@ Rejects when:
|
|
|
116
154
|
|
|
117
155
|
- `protocols?: string | string[]` — WebSocket subprotocols to request
|
|
118
156
|
- `headers?: Record<string, string>` — custom handshake headers in Node.js
|
|
157
|
+
- `timeout?: number` — connection timeout in milliseconds; rejects if the connection is not established within this time
|
|
158
|
+
- `signal?: AbortSignal` — an abort signal to cancel the connection attempt
|
|
119
159
|
|
|
120
160
|
> In browsers, passing `headers` throws because the native WebSocket API does not support custom headers.
|
|
121
161
|
|
|
@@ -182,6 +222,8 @@ Behavior:
|
|
|
182
222
|
interface ConnectOptions {
|
|
183
223
|
protocols?: string | string[];
|
|
184
224
|
headers?: Record<string, string>;
|
|
225
|
+
timeout?: number;
|
|
226
|
+
signal?: AbortSignal;
|
|
185
227
|
}
|
|
186
228
|
```
|
|
187
229
|
|
|
@@ -192,11 +234,24 @@ Connection-time options.
|
|
|
192
234
|
```ts
|
|
193
235
|
interface ClientOptions {
|
|
194
236
|
maxBufferSize?: number;
|
|
237
|
+
keepAlive?: KeepAliveOptions;
|
|
195
238
|
}
|
|
196
239
|
```
|
|
197
240
|
|
|
198
241
|
Client-level configuration.
|
|
199
242
|
|
|
243
|
+
### `KeepAliveOptions`
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
interface KeepAliveOptions {
|
|
247
|
+
interval: number;
|
|
248
|
+
timeout?: number;
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
- `interval` — milliseconds between pings
|
|
253
|
+
- `timeout` — milliseconds to wait for a pong before terminating the connection (default: `interval`)
|
|
254
|
+
|
|
200
255
|
### `WebSocketMessage`
|
|
201
256
|
|
|
202
257
|
```ts
|
|
@@ -276,7 +331,7 @@ This is useful when you want a stream-like consumer loop without manually callin
|
|
|
276
331
|
|
|
277
332
|
All core operations are async and communicate failure by rejecting:
|
|
278
333
|
|
|
279
|
-
- `connect()` rejects on invalid state, connection failure,
|
|
334
|
+
- `connect()` rejects on invalid state, connection failure, early close, timeout, or abort
|
|
280
335
|
- `send()` rejects when called before the socket is open or when the adapter fails to send
|
|
281
336
|
- `receive()` rejects when the client is not in a receivable state and no buffered messages remain
|
|
282
337
|
- `close()` rejects for invalid close codes
|
|
@@ -324,6 +379,49 @@ When the buffer is full:
|
|
|
324
379
|
|
|
325
380
|
This makes buffering predictable for bursty message streams while keeping the public API simple.
|
|
326
381
|
|
|
382
|
+
## Connection Timeout and Abort
|
|
383
|
+
|
|
384
|
+
Use `timeout` to reject if the connection isn't established within a deadline:
|
|
385
|
+
|
|
386
|
+
```ts
|
|
387
|
+
await client.connect("wss://example.com/ws", { timeout: 5000 });
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
Use `signal` to cancel a connection attempt at any time:
|
|
391
|
+
|
|
392
|
+
```ts
|
|
393
|
+
const controller = new AbortController();
|
|
394
|
+
setTimeout(() => controller.abort(), 3000);
|
|
395
|
+
|
|
396
|
+
await client.connect("wss://example.com/ws", { signal: controller.signal });
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
Both can be combined:
|
|
400
|
+
|
|
401
|
+
```ts
|
|
402
|
+
await client.connect("wss://example.com/ws", {
|
|
403
|
+
timeout: 10000,
|
|
404
|
+
signal: controller.signal,
|
|
405
|
+
});
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
## Keep-Alive (Node.js)
|
|
409
|
+
|
|
410
|
+
Enable automatic ping/pong to detect dead connections:
|
|
411
|
+
|
|
412
|
+
```ts
|
|
413
|
+
const client = new WebSocketClient({
|
|
414
|
+
keepAlive: { interval: 30000, timeout: 5000 },
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
await client.connect("wss://example.com/ws");
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
- Sends a ping every `interval` milliseconds
|
|
421
|
+
- If no pong is received within `timeout` milliseconds, the connection is terminated
|
|
422
|
+
- `timeout` defaults to `interval` if omitted
|
|
423
|
+
- **Not available in browsers** — the constructor throws if `keepAlive` is configured in a browser environment
|
|
424
|
+
|
|
327
425
|
## Building from Source
|
|
328
426
|
|
|
329
427
|
```bash
|
package/dist/browser/index.js
CHANGED
|
@@ -10,7 +10,17 @@ function socketSend(socket, data) {
|
|
|
10
10
|
return Promise.reject(new Error("WebSocket is not open"));
|
|
11
11
|
}
|
|
12
12
|
try {
|
|
13
|
-
|
|
13
|
+
if (ArrayBuffer.isView(data)) {
|
|
14
|
+
if (data.buffer instanceof SharedArrayBuffer) {
|
|
15
|
+
throw new Error("SharedArrayBuffer-backed views are not supported. " +
|
|
16
|
+
"Copy into a regular ArrayBuffer before sending.");
|
|
17
|
+
}
|
|
18
|
+
// Zero-copy: create a Uint8Array view over the same ArrayBuffer
|
|
19
|
+
socket.send(new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
socket.send(data);
|
|
23
|
+
}
|
|
14
24
|
return Promise.resolve();
|
|
15
25
|
}
|
|
16
26
|
catch (err) {
|
|
@@ -42,6 +52,15 @@ function attachListeners(socket, onOpen, onMessage, onClose, onError) {
|
|
|
42
52
|
function socketClose(socket, code, reason) {
|
|
43
53
|
socket.close(code, reason);
|
|
44
54
|
}
|
|
55
|
+
function socketTerminate(socket) {
|
|
56
|
+
socket.close();
|
|
57
|
+
}
|
|
58
|
+
function socketPing(_socket) {
|
|
59
|
+
throw new Error("Ping is not supported in browsers.");
|
|
60
|
+
}
|
|
61
|
+
function attachPongListener(_socket, _onPong) {
|
|
62
|
+
return () => { };
|
|
63
|
+
}
|
|
45
64
|
|
|
46
65
|
/**
|
|
47
66
|
* Imperative WebSocket client that works in both browser and Node.js.
|
|
@@ -62,7 +81,17 @@ class WebSocketClient {
|
|
|
62
81
|
this.terminalError = null;
|
|
63
82
|
this.closeInfo = null;
|
|
64
83
|
this.removeListeners = null;
|
|
84
|
+
this.keepAliveTimer = null;
|
|
85
|
+
this.pongTimer = null;
|
|
86
|
+
this.removePongListener = null;
|
|
87
|
+
this.connectionId = 0;
|
|
65
88
|
this.maxBufferSize = options?.maxBufferSize ?? 0;
|
|
89
|
+
if (options?.keepAlive) {
|
|
90
|
+
{
|
|
91
|
+
throw new Error("keepAlive is not supported in browsers. " +
|
|
92
|
+
"The browser handles WebSocket ping/pong at the protocol level automatically.");
|
|
93
|
+
}
|
|
94
|
+
}
|
|
66
95
|
}
|
|
67
96
|
/** Current connection state. */
|
|
68
97
|
get readyState() {
|
|
@@ -72,16 +101,41 @@ class WebSocketClient {
|
|
|
72
101
|
get lastCloseInfo() {
|
|
73
102
|
return this.closeInfo;
|
|
74
103
|
}
|
|
104
|
+
/** The negotiated subprotocol, or empty string if none. */
|
|
105
|
+
get protocol() {
|
|
106
|
+
return this.socket?.protocol ?? "";
|
|
107
|
+
}
|
|
108
|
+
/** The URL of the WebSocket connection. */
|
|
109
|
+
get url() {
|
|
110
|
+
return this.socket?.url ?? "";
|
|
111
|
+
}
|
|
112
|
+
/** The number of bytes of data queued for sending. */
|
|
113
|
+
get bufferedAmount() {
|
|
114
|
+
return this.socket?.bufferedAmount ?? 0;
|
|
115
|
+
}
|
|
116
|
+
/** The extensions negotiated by the server. */
|
|
117
|
+
get extensions() {
|
|
118
|
+
return this.socket?.extensions ?? "";
|
|
119
|
+
}
|
|
75
120
|
/**
|
|
76
121
|
* Connect to a WebSocket server.
|
|
77
122
|
* Resolves when the connection is open. Rejects on error.
|
|
78
123
|
*/
|
|
79
124
|
connect(url, options) {
|
|
80
|
-
if (this.state !== "idle" &&
|
|
125
|
+
if (this.state !== "idle" &&
|
|
126
|
+
this.state !== "closed" &&
|
|
127
|
+
this.state !== "errored") {
|
|
81
128
|
return Promise.reject(new Error(`Cannot connect: client is in "${this.state}" state`));
|
|
82
129
|
}
|
|
130
|
+
if (options?.timeout !== undefined && options.timeout <= 0) {
|
|
131
|
+
return Promise.reject(new Error("timeout must be greater than 0."));
|
|
132
|
+
}
|
|
133
|
+
if (options?.signal?.aborted) {
|
|
134
|
+
return Promise.reject(new Error("Connection aborted."));
|
|
135
|
+
}
|
|
83
136
|
this.reset();
|
|
84
137
|
this.state = "connecting";
|
|
138
|
+
const currentConnectionId = ++this.connectionId;
|
|
85
139
|
return new Promise((resolve, reject) => {
|
|
86
140
|
try {
|
|
87
141
|
this.socket = createWebSocket(url, options);
|
|
@@ -89,19 +143,63 @@ class WebSocketClient {
|
|
|
89
143
|
}
|
|
90
144
|
catch (err) {
|
|
91
145
|
this.state = "errored";
|
|
92
|
-
this.terminalError =
|
|
146
|
+
this.terminalError =
|
|
147
|
+
err instanceof Error ? err : new Error(String(err));
|
|
93
148
|
reject(this.terminalError);
|
|
94
149
|
return;
|
|
95
150
|
}
|
|
96
151
|
let settled = false;
|
|
152
|
+
let timeoutId = null;
|
|
153
|
+
const settle = (fn) => {
|
|
154
|
+
if (settled)
|
|
155
|
+
return;
|
|
156
|
+
settled = true;
|
|
157
|
+
if (timeoutId !== null) {
|
|
158
|
+
clearTimeout(timeoutId);
|
|
159
|
+
timeoutId = null;
|
|
160
|
+
}
|
|
161
|
+
if (options?.signal) {
|
|
162
|
+
options.signal.removeEventListener("abort", onAbort);
|
|
163
|
+
}
|
|
164
|
+
fn();
|
|
165
|
+
};
|
|
166
|
+
const onAbort = () => {
|
|
167
|
+
settle(() => {
|
|
168
|
+
this.state = "closed";
|
|
169
|
+
this.terminalError = new Error("Connection aborted.");
|
|
170
|
+
if (this.socket) {
|
|
171
|
+
socketTerminate(this.socket);
|
|
172
|
+
}
|
|
173
|
+
// Don't call cleanup() here — socketTerminate() emits
|
|
174
|
+
// error/close asynchronously; let onClose handle cleanup.
|
|
175
|
+
reject(this.terminalError);
|
|
176
|
+
});
|
|
177
|
+
};
|
|
178
|
+
if (options?.timeout !== undefined) {
|
|
179
|
+
timeoutId = setTimeout(() => {
|
|
180
|
+
settle(() => {
|
|
181
|
+
this.state = "closed";
|
|
182
|
+
this.terminalError = new Error(`Connection timed out after ${options.timeout}ms.`);
|
|
183
|
+
if (this.socket) {
|
|
184
|
+
socketTerminate(this.socket);
|
|
185
|
+
}
|
|
186
|
+
// Don't call cleanup() here — socketTerminate() emits
|
|
187
|
+
// error/close asynchronously; let onClose handle cleanup.
|
|
188
|
+
reject(this.terminalError);
|
|
189
|
+
});
|
|
190
|
+
}, options.timeout);
|
|
191
|
+
}
|
|
192
|
+
if (options?.signal) {
|
|
193
|
+
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
194
|
+
}
|
|
97
195
|
this.removeListeners = attachListeners(this.socket,
|
|
98
196
|
// onOpen
|
|
99
197
|
() => {
|
|
100
|
-
|
|
101
|
-
settled = true;
|
|
198
|
+
settle(() => {
|
|
102
199
|
this.state = "open";
|
|
200
|
+
this.startKeepAlive();
|
|
103
201
|
resolve();
|
|
104
|
-
}
|
|
202
|
+
});
|
|
105
203
|
},
|
|
106
204
|
// onMessage
|
|
107
205
|
(data, binary) => {
|
|
@@ -109,14 +207,16 @@ class WebSocketClient {
|
|
|
109
207
|
},
|
|
110
208
|
// onClose
|
|
111
209
|
(code, reason, wasClean) => {
|
|
210
|
+
// Ignore close events from a stale connection (e.g., after
|
|
211
|
+
// timeout/abort triggered a reconnect on the same client).
|
|
212
|
+
if (currentConnectionId !== this.connectionId)
|
|
213
|
+
return;
|
|
112
214
|
this.closeInfo = { code, reason, wasClean };
|
|
113
|
-
this.state;
|
|
114
215
|
this.state = "closed";
|
|
115
216
|
this.cleanup();
|
|
116
|
-
|
|
117
|
-
settled = true;
|
|
217
|
+
settle(() => {
|
|
118
218
|
reject(new Error(`WebSocket closed before opening (code: ${code}, reason: ${reason})`));
|
|
119
|
-
}
|
|
219
|
+
});
|
|
120
220
|
// Only reject pending waiters once buffer is drained
|
|
121
221
|
if (this.buffer.length === 0) {
|
|
122
222
|
this.rejectAllWaiters(new Error(`WebSocket closed (code: ${code}, reason: ${reason})`));
|
|
@@ -124,11 +224,13 @@ class WebSocketClient {
|
|
|
124
224
|
},
|
|
125
225
|
// onError
|
|
126
226
|
(error) => {
|
|
227
|
+
// Ignore error events from a stale connection.
|
|
228
|
+
if (currentConnectionId !== this.connectionId)
|
|
229
|
+
return;
|
|
127
230
|
this.terminalError = error;
|
|
128
|
-
|
|
129
|
-
settled = true;
|
|
231
|
+
settle(() => {
|
|
130
232
|
reject(error);
|
|
131
|
-
}
|
|
233
|
+
});
|
|
132
234
|
// Reject any pending receive() waiters immediately
|
|
133
235
|
this.rejectAllWaiters(error);
|
|
134
236
|
// Don't call cleanup() here — per spec, a close event always
|
|
@@ -176,7 +278,9 @@ class WebSocketClient {
|
|
|
176
278
|
* Resolves when the close handshake completes.
|
|
177
279
|
*/
|
|
178
280
|
close(code, reason) {
|
|
179
|
-
if (this.state === "closed" ||
|
|
281
|
+
if (this.state === "closed" ||
|
|
282
|
+
this.state === "idle" ||
|
|
283
|
+
this.state === "errored") {
|
|
180
284
|
return Promise.resolve();
|
|
181
285
|
}
|
|
182
286
|
if (!this.socket) {
|
|
@@ -186,7 +290,9 @@ class WebSocketClient {
|
|
|
186
290
|
// Already closing — wait for the close event via a one-shot listener
|
|
187
291
|
return new Promise((resolve) => {
|
|
188
292
|
if (this.socket) {
|
|
189
|
-
this.socket.addEventListener("close", () => resolve(), {
|
|
293
|
+
this.socket.addEventListener("close", () => resolve(), {
|
|
294
|
+
once: true,
|
|
295
|
+
});
|
|
190
296
|
}
|
|
191
297
|
else {
|
|
192
298
|
resolve();
|
|
@@ -251,11 +357,50 @@ class WebSocketClient {
|
|
|
251
357
|
waiter.reject(error);
|
|
252
358
|
}
|
|
253
359
|
}
|
|
360
|
+
startKeepAlive() {
|
|
361
|
+
if (!this.keepAliveConfig || !this.socket)
|
|
362
|
+
return;
|
|
363
|
+
const { interval, timeout } = this.keepAliveConfig;
|
|
364
|
+
const pongTimeout = timeout ?? interval;
|
|
365
|
+
this.removePongListener = attachPongListener(this.socket);
|
|
366
|
+
this.keepAliveTimer = setInterval(() => {
|
|
367
|
+
if (this.state !== "open" || !this.socket)
|
|
368
|
+
return;
|
|
369
|
+
socketPing(this.socket);
|
|
370
|
+
// Clear any existing pong watchdog before starting a new one
|
|
371
|
+
// to prevent multiple timers when timeout > interval.
|
|
372
|
+
if (this.pongTimer !== null) {
|
|
373
|
+
clearTimeout(this.pongTimer);
|
|
374
|
+
}
|
|
375
|
+
this.pongTimer = setTimeout(() => {
|
|
376
|
+
if (this.state === "open" && this.socket) {
|
|
377
|
+
this.terminalError = new Error(`Keep-alive timeout: no pong received within ${pongTimeout}ms.`);
|
|
378
|
+
socketTerminate(this.socket);
|
|
379
|
+
}
|
|
380
|
+
}, pongTimeout);
|
|
381
|
+
}, interval);
|
|
382
|
+
}
|
|
383
|
+
stopKeepAlive() {
|
|
384
|
+
if (this.keepAliveTimer !== null) {
|
|
385
|
+
clearInterval(this.keepAliveTimer);
|
|
386
|
+
this.keepAliveTimer = null;
|
|
387
|
+
}
|
|
388
|
+
if (this.pongTimer !== null) {
|
|
389
|
+
clearTimeout(this.pongTimer);
|
|
390
|
+
this.pongTimer = null;
|
|
391
|
+
}
|
|
392
|
+
if (this.removePongListener) {
|
|
393
|
+
this.removePongListener();
|
|
394
|
+
this.removePongListener = null;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
254
397
|
cleanup() {
|
|
398
|
+
this.stopKeepAlive();
|
|
255
399
|
if (this.removeListeners) {
|
|
256
400
|
this.removeListeners();
|
|
257
401
|
this.removeListeners = null;
|
|
258
402
|
}
|
|
403
|
+
this.socket = null;
|
|
259
404
|
}
|
|
260
405
|
reset() {
|
|
261
406
|
this.socket = null;
|
|
@@ -264,6 +409,7 @@ class WebSocketClient {
|
|
|
264
409
|
this.terminalError = null;
|
|
265
410
|
this.closeInfo = null;
|
|
266
411
|
this.removeListeners = null;
|
|
412
|
+
this.stopKeepAlive();
|
|
267
413
|
}
|
|
268
414
|
}
|
|
269
415
|
|