connectbase-client 3.32.0 → 3.34.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 +41 -0
- package/README.md +11 -3
- package/dist/connect-base.umd.js +5 -4
- package/dist/index.d.mts +69 -9
- package/dist/index.d.ts +69 -9
- package/dist/index.js +230 -7
- package/dist/index.mjs +230 -7
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -2913,13 +2913,25 @@ var RealtimeAPI = class {
|
|
|
2913
2913
|
this.state = "disconnected";
|
|
2914
2914
|
this._connectionId = null;
|
|
2915
2915
|
this._appId = null;
|
|
2916
|
+
/**
|
|
2917
|
+
* 현재 활성 transport.
|
|
2918
|
+
* - `'ws'`: WebSocket 정상 연결
|
|
2919
|
+
* - `'sse'`: WS 핸드셰이크 실패로 HTTP SSE fallback 모드 (AI `stream()` 만 지원)
|
|
2920
|
+
* - `null`: 미연결
|
|
2921
|
+
*/
|
|
2922
|
+
this.activeTransport = null;
|
|
2923
|
+
/** SSE fallback 모드 활성 여부. true 면 백그라운드 WS 재연결 머신을 정지한다. */
|
|
2924
|
+
this.sseFallbackActive = false;
|
|
2925
|
+
/** SSE fallback 스트림의 AbortController (sessionId → controller) — stop()/disconnect() 에서 중단용 */
|
|
2926
|
+
this.sseSessions = /* @__PURE__ */ new Map();
|
|
2916
2927
|
this.options = {
|
|
2917
2928
|
maxRetries: 5,
|
|
2918
2929
|
retryInterval: 1e3,
|
|
2919
2930
|
userId: "",
|
|
2920
2931
|
accessToken: "",
|
|
2921
2932
|
timeout: 3e4,
|
|
2922
|
-
debug: false
|
|
2933
|
+
debug: false,
|
|
2934
|
+
fallback: "sse"
|
|
2923
2935
|
};
|
|
2924
2936
|
this.retryCount = 0;
|
|
2925
2937
|
this.pendingRequests = /* @__PURE__ */ new Map();
|
|
@@ -2948,6 +2960,13 @@ var RealtimeAPI = class {
|
|
|
2948
2960
|
get appId() {
|
|
2949
2961
|
return this._appId;
|
|
2950
2962
|
}
|
|
2963
|
+
/**
|
|
2964
|
+
* 현재 활성 transport (`'ws'` | `'sse'` | `null`).
|
|
2965
|
+
* `'sse'` 이면 WS 가 차단되어 fallback 모드로 동작 중이며 AI `stream()` 만 가능하다.
|
|
2966
|
+
*/
|
|
2967
|
+
get transport() {
|
|
2968
|
+
return this.activeTransport;
|
|
2969
|
+
}
|
|
2951
2970
|
/**
|
|
2952
2971
|
* WebSocket 연결
|
|
2953
2972
|
* @param options 연결 옵션
|
|
@@ -2968,10 +2987,36 @@ var RealtimeAPI = class {
|
|
|
2968
2987
|
this.connectPromise = this.doConnect();
|
|
2969
2988
|
try {
|
|
2970
2989
|
await this.connectPromise;
|
|
2990
|
+
} catch (e) {
|
|
2991
|
+
const hasAuth = !!(this.options.accessToken || this.http.getPublicKey());
|
|
2992
|
+
if (this.options.fallback !== "none" && hasAuth) {
|
|
2993
|
+
this.activateSseFallback(e);
|
|
2994
|
+
} else {
|
|
2995
|
+
throw e;
|
|
2996
|
+
}
|
|
2971
2997
|
} finally {
|
|
2972
2998
|
this.connectPromise = null;
|
|
2973
2999
|
}
|
|
2974
3000
|
}
|
|
3001
|
+
/**
|
|
3002
|
+
* SSE fallback 모드로 전환한다. WS 는 사용 불가하지만 state 는 'connected' 로 두어
|
|
3003
|
+
* (degraded) AI stream() 이 SSE 로 동작하게 한다. 백그라운드 WS 재연결 머신은 정지한다.
|
|
3004
|
+
*/
|
|
3005
|
+
activateSseFallback(cause) {
|
|
3006
|
+
if (this.ws) {
|
|
3007
|
+
try {
|
|
3008
|
+
this.ws.close();
|
|
3009
|
+
} catch {
|
|
3010
|
+
}
|
|
3011
|
+
this.ws = null;
|
|
3012
|
+
}
|
|
3013
|
+
this.sseFallbackActive = true;
|
|
3014
|
+
this.activeTransport = "sse";
|
|
3015
|
+
this.state = "connected";
|
|
3016
|
+
this.retryCount = 0;
|
|
3017
|
+
this.log(`WS unavailable (${cause instanceof Error ? cause.message : String(cause)}); using SSE fallback transport for AI streaming`);
|
|
3018
|
+
this.notifyStateChange();
|
|
3019
|
+
}
|
|
2975
3020
|
/**
|
|
2976
3021
|
* 연결 해제
|
|
2977
3022
|
*/
|
|
@@ -2994,6 +3039,15 @@ var RealtimeAPI = class {
|
|
|
2994
3039
|
}
|
|
2995
3040
|
});
|
|
2996
3041
|
this.streamSessions.clear();
|
|
3042
|
+
this.sseSessions.forEach((controller) => {
|
|
3043
|
+
try {
|
|
3044
|
+
controller.abort();
|
|
3045
|
+
} catch {
|
|
3046
|
+
}
|
|
3047
|
+
});
|
|
3048
|
+
this.sseSessions.clear();
|
|
3049
|
+
this.sseFallbackActive = false;
|
|
3050
|
+
this.activeTransport = null;
|
|
2997
3051
|
this.presenceHandlers = [];
|
|
2998
3052
|
this.presenceSubscriptions.clear();
|
|
2999
3053
|
this.typingHandlers.clear();
|
|
@@ -3007,6 +3061,7 @@ var RealtimeAPI = class {
|
|
|
3007
3061
|
* 카테고리 구독
|
|
3008
3062
|
*/
|
|
3009
3063
|
async subscribe(category, options = {}) {
|
|
3064
|
+
this.assertBidirectional("subscribe");
|
|
3010
3065
|
if (this.state !== "connected") {
|
|
3011
3066
|
throw new Error("Not connected. Call connect() first.");
|
|
3012
3067
|
}
|
|
@@ -3067,6 +3122,7 @@ var RealtimeAPI = class {
|
|
|
3067
3122
|
* @param options 전송 옵션 (includeSelf: 발신자도 메시지 수신 여부, 기본값 true)
|
|
3068
3123
|
*/
|
|
3069
3124
|
async sendMessage(category, data, options = {}) {
|
|
3125
|
+
this.assertBidirectional("sendMessage");
|
|
3070
3126
|
if (this.state !== "connected") {
|
|
3071
3127
|
throw new Error("Not connected");
|
|
3072
3128
|
}
|
|
@@ -3084,6 +3140,7 @@ var RealtimeAPI = class {
|
|
|
3084
3140
|
* 히스토리 조회
|
|
3085
3141
|
*/
|
|
3086
3142
|
async getHistory(category, limit) {
|
|
3143
|
+
this.assertBidirectional("getHistory");
|
|
3087
3144
|
if (this.state !== "connected") {
|
|
3088
3145
|
throw new Error("Not connected");
|
|
3089
3146
|
}
|
|
@@ -3139,9 +3196,22 @@ var RealtimeAPI = class {
|
|
|
3139
3196
|
* ```
|
|
3140
3197
|
*/
|
|
3141
3198
|
async stream(messages, handlers, options = {}) {
|
|
3142
|
-
|
|
3143
|
-
|
|
3199
|
+
const canWs = this.state === "connected" && this.activeTransport === "ws" && this.ws?.readyState === WebSocket.OPEN;
|
|
3200
|
+
if (canWs) {
|
|
3201
|
+
try {
|
|
3202
|
+
return this.streamViaWS(messages, handlers, options);
|
|
3203
|
+
} catch (e) {
|
|
3204
|
+
if (this.options.fallback === "none") throw e;
|
|
3205
|
+
this.log(`WS stream send failed (${e instanceof Error ? e.message : String(e)}); falling back to SSE`);
|
|
3206
|
+
}
|
|
3207
|
+
}
|
|
3208
|
+
if (this.options.fallback !== "none") {
|
|
3209
|
+
return this.streamViaSSE(messages, handlers, options);
|
|
3144
3210
|
}
|
|
3211
|
+
throw new Error("Not connected. Call connect() first.");
|
|
3212
|
+
}
|
|
3213
|
+
/** WebSocket 으로 AI 스트리밍 (기본 경로) */
|
|
3214
|
+
streamViaWS(messages, handlers, options) {
|
|
3145
3215
|
const requestId = this.generateRequestId();
|
|
3146
3216
|
const sessionId = options.sessionId || this.generateRequestId();
|
|
3147
3217
|
this.streamSessions.set(sessionId, {
|
|
@@ -3176,11 +3246,130 @@ var RealtimeAPI = class {
|
|
|
3176
3246
|
}
|
|
3177
3247
|
};
|
|
3178
3248
|
}
|
|
3249
|
+
/**
|
|
3250
|
+
* HTTP SSE (`/v1/public/ai/chat/stream`) 로 AI 스트리밍 — WS 가 차단된 네트워크용 fallback.
|
|
3251
|
+
*
|
|
3252
|
+
* SSE 는 단방향이라 WS 의 `StreamDoneData` 토큰 카운트(total/prompt)를 받지 못하므로
|
|
3253
|
+
* `totalTokens`/`promptTokens` 는 0 으로 채워진다(`fullText`/`duration` 은 정확). `mcpGroup`
|
|
3254
|
+
* (MCP 도구) 은 SSE fallback 에서 미지원 — 지정 시 무시되고 도구 없이 스트리밍한다.
|
|
3255
|
+
*/
|
|
3256
|
+
streamViaSSE(messages, handlers, options) {
|
|
3257
|
+
const sessionId = options.sessionId || this.generateRequestId();
|
|
3258
|
+
const controller = new AbortController();
|
|
3259
|
+
this.sseSessions.set(sessionId, controller);
|
|
3260
|
+
if (options.mcpGroup) {
|
|
3261
|
+
this.log("mcpGroup is not supported in SSE fallback mode; streaming without MCP tools");
|
|
3262
|
+
}
|
|
3263
|
+
this.log(`Streaming via SSE fallback (session ${sessionId})`);
|
|
3264
|
+
void this.runSseStream(sessionId, messages, handlers, options, controller.signal);
|
|
3265
|
+
return {
|
|
3266
|
+
sessionId,
|
|
3267
|
+
stop: async () => {
|
|
3268
|
+
await this.stopStream(sessionId);
|
|
3269
|
+
}
|
|
3270
|
+
};
|
|
3271
|
+
}
|
|
3272
|
+
async runSseStream(sessionId, messages, handlers, options, signal) {
|
|
3273
|
+
const startedAt = Date.now();
|
|
3274
|
+
let tokenIndex = 0;
|
|
3275
|
+
let fullText = "";
|
|
3276
|
+
let reader;
|
|
3277
|
+
const wireMessages = options.system ? [{ role: "system", content: options.system }, ...messages] : messages;
|
|
3278
|
+
const body = {
|
|
3279
|
+
messages: wireMessages,
|
|
3280
|
+
provider: options.provider,
|
|
3281
|
+
model: options.model,
|
|
3282
|
+
temperature: options.temperature,
|
|
3283
|
+
maxTokens: options.maxTokens
|
|
3284
|
+
};
|
|
3285
|
+
try {
|
|
3286
|
+
const response = await this.http.fetchRaw("/v1/public/ai/chat/stream", {
|
|
3287
|
+
method: "POST",
|
|
3288
|
+
headers: { "Content-Type": "application/json" },
|
|
3289
|
+
body: JSON.stringify(body),
|
|
3290
|
+
signal
|
|
3291
|
+
});
|
|
3292
|
+
if (!response.ok) {
|
|
3293
|
+
const errData = await response.json().catch(() => ({ error: "Stream request failed" }));
|
|
3294
|
+
handlers.onError?.(new Error(errData.error || errData.message || "Stream request failed"));
|
|
3295
|
+
return;
|
|
3296
|
+
}
|
|
3297
|
+
reader = response.body?.getReader();
|
|
3298
|
+
if (!reader) {
|
|
3299
|
+
handlers.onError?.(new Error("ReadableStream not supported"));
|
|
3300
|
+
return;
|
|
3301
|
+
}
|
|
3302
|
+
const decoder = new TextDecoder();
|
|
3303
|
+
let buffer = "";
|
|
3304
|
+
while (true) {
|
|
3305
|
+
const { done, value } = await reader.read();
|
|
3306
|
+
if (done) break;
|
|
3307
|
+
buffer += decoder.decode(value, { stream: true });
|
|
3308
|
+
const lines = buffer.split("\n");
|
|
3309
|
+
buffer = lines.pop() || "";
|
|
3310
|
+
for (const line of lines) {
|
|
3311
|
+
if (!line.startsWith("data: ")) continue;
|
|
3312
|
+
const data = line.slice(6).trim();
|
|
3313
|
+
if (data === "[DONE]") {
|
|
3314
|
+
handlers.onDone?.({ sessionId, fullText, totalTokens: 0, promptTokens: 0, duration: Date.now() - startedAt });
|
|
3315
|
+
return;
|
|
3316
|
+
}
|
|
3317
|
+
try {
|
|
3318
|
+
const ev = JSON.parse(data);
|
|
3319
|
+
if (ev.error) {
|
|
3320
|
+
handlers.onError?.(new Error(ev.message || ev.error || "stream error"));
|
|
3321
|
+
return;
|
|
3322
|
+
}
|
|
3323
|
+
if (ev.type === "tool_start") {
|
|
3324
|
+
handlers.onToolCall?.(ev.name || "", ev.arguments || {}, tokenIndex);
|
|
3325
|
+
continue;
|
|
3326
|
+
}
|
|
3327
|
+
if (ev.type === "tool_end") {
|
|
3328
|
+
handlers.onToolResult?.(ev.name || "", ev.success !== false, ev.durationMs || 0, tokenIndex);
|
|
3329
|
+
continue;
|
|
3330
|
+
}
|
|
3331
|
+
if (ev.type === "sources" || ev.type === "searching" || ev.type === "heartbeat") {
|
|
3332
|
+
continue;
|
|
3333
|
+
}
|
|
3334
|
+
if (ev.content) {
|
|
3335
|
+
fullText += ev.content;
|
|
3336
|
+
handlers.onToken?.(ev.content, tokenIndex++);
|
|
3337
|
+
}
|
|
3338
|
+
if (ev.done) {
|
|
3339
|
+
handlers.onDone?.({ sessionId, fullText, totalTokens: 0, promptTokens: 0, duration: Date.now() - startedAt });
|
|
3340
|
+
return;
|
|
3341
|
+
}
|
|
3342
|
+
} catch {
|
|
3343
|
+
}
|
|
3344
|
+
}
|
|
3345
|
+
}
|
|
3346
|
+
} catch (err) {
|
|
3347
|
+
const aborted = signal.aborted || err instanceof DOMException && err.name === "AbortError" || typeof err === "object" && err !== null && err.name === "AbortError";
|
|
3348
|
+
if (!aborted) {
|
|
3349
|
+
handlers.onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
3350
|
+
}
|
|
3351
|
+
} finally {
|
|
3352
|
+
this.sseSessions.delete(sessionId);
|
|
3353
|
+
if (reader) {
|
|
3354
|
+
try {
|
|
3355
|
+
await reader.cancel();
|
|
3356
|
+
} catch {
|
|
3357
|
+
}
|
|
3358
|
+
}
|
|
3359
|
+
}
|
|
3360
|
+
}
|
|
3179
3361
|
/**
|
|
3180
3362
|
* 스트리밍 중지
|
|
3181
3363
|
*/
|
|
3182
3364
|
async stopStream(sessionId) {
|
|
3183
|
-
|
|
3365
|
+
const sse = this.sseSessions.get(sessionId);
|
|
3366
|
+
if (sse) {
|
|
3367
|
+
sse.abort();
|
|
3368
|
+
this.sseSessions.delete(sessionId);
|
|
3369
|
+
return;
|
|
3370
|
+
}
|
|
3371
|
+
if (this.state !== "connected" || !this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
3372
|
+
this.streamSessions.delete(sessionId);
|
|
3184
3373
|
return;
|
|
3185
3374
|
}
|
|
3186
3375
|
const requestId = this.generateRequestId();
|
|
@@ -3239,6 +3428,7 @@ var RealtimeAPI = class {
|
|
|
3239
3428
|
* ```
|
|
3240
3429
|
*/
|
|
3241
3430
|
async setPresence(status, options = {}) {
|
|
3431
|
+
this.assertBidirectional("setPresence");
|
|
3242
3432
|
if (this.state !== "connected") {
|
|
3243
3433
|
throw new Error("Not connected");
|
|
3244
3434
|
}
|
|
@@ -3266,6 +3456,7 @@ var RealtimeAPI = class {
|
|
|
3266
3456
|
* ```
|
|
3267
3457
|
*/
|
|
3268
3458
|
async setPresenceOnDisconnect(status, metadata) {
|
|
3459
|
+
this.assertBidirectional("setPresenceOnDisconnect");
|
|
3269
3460
|
if (this.state !== "connected") {
|
|
3270
3461
|
throw new Error("Not connected");
|
|
3271
3462
|
}
|
|
@@ -3289,6 +3480,7 @@ var RealtimeAPI = class {
|
|
|
3289
3480
|
* ```
|
|
3290
3481
|
*/
|
|
3291
3482
|
async getPresence(userId) {
|
|
3483
|
+
this.assertBidirectional("getPresence");
|
|
3292
3484
|
if (this.state !== "connected") {
|
|
3293
3485
|
throw new Error("Not connected");
|
|
3294
3486
|
}
|
|
@@ -3319,6 +3511,7 @@ var RealtimeAPI = class {
|
|
|
3319
3511
|
* ```
|
|
3320
3512
|
*/
|
|
3321
3513
|
async getPresenceMany(userIds) {
|
|
3514
|
+
this.assertBidirectional("getPresenceMany");
|
|
3322
3515
|
if (this.state !== "connected") {
|
|
3323
3516
|
throw new Error("Not connected");
|
|
3324
3517
|
}
|
|
@@ -3358,6 +3551,7 @@ var RealtimeAPI = class {
|
|
|
3358
3551
|
* ```
|
|
3359
3552
|
*/
|
|
3360
3553
|
async subscribePresence(userId, handler) {
|
|
3554
|
+
this.assertBidirectional("subscribePresence");
|
|
3361
3555
|
if (this.state !== "connected") {
|
|
3362
3556
|
throw new Error("Not connected");
|
|
3363
3557
|
}
|
|
@@ -3427,6 +3621,7 @@ var RealtimeAPI = class {
|
|
|
3427
3621
|
* ```
|
|
3428
3622
|
*/
|
|
3429
3623
|
async startTyping(roomId) {
|
|
3624
|
+
this.assertBidirectional("startTyping");
|
|
3430
3625
|
if (this.state !== "connected") {
|
|
3431
3626
|
throw new Error("Not connected");
|
|
3432
3627
|
}
|
|
@@ -3451,6 +3646,7 @@ var RealtimeAPI = class {
|
|
|
3451
3646
|
* ```
|
|
3452
3647
|
*/
|
|
3453
3648
|
async stopTyping(roomId) {
|
|
3649
|
+
this.assertBidirectional("stopTyping");
|
|
3454
3650
|
if (this.state !== "connected") {
|
|
3455
3651
|
throw new Error("Not connected");
|
|
3456
3652
|
}
|
|
@@ -3480,6 +3676,7 @@ var RealtimeAPI = class {
|
|
|
3480
3676
|
* ```
|
|
3481
3677
|
*/
|
|
3482
3678
|
async onTypingChange(roomId, handler) {
|
|
3679
|
+
this.assertBidirectional("onTypingChange");
|
|
3483
3680
|
if (this.state !== "connected") {
|
|
3484
3681
|
throw new Error("Not connected");
|
|
3485
3682
|
}
|
|
@@ -3527,6 +3724,7 @@ var RealtimeAPI = class {
|
|
|
3527
3724
|
* ```
|
|
3528
3725
|
*/
|
|
3529
3726
|
async markRead(category, messageIds) {
|
|
3727
|
+
this.assertBidirectional("markRead");
|
|
3530
3728
|
if (this.state !== "connected") {
|
|
3531
3729
|
throw new Error("Not connected");
|
|
3532
3730
|
}
|
|
@@ -3675,6 +3873,8 @@ var RealtimeAPI = class {
|
|
|
3675
3873
|
this._connectionId = data.connection_id;
|
|
3676
3874
|
this._appId = data.app_id;
|
|
3677
3875
|
this.state = "connected";
|
|
3876
|
+
this.activeTransport = "ws";
|
|
3877
|
+
this.sseFallbackActive = false;
|
|
3678
3878
|
this.retryCount = 0;
|
|
3679
3879
|
this.notifyStateChange();
|
|
3680
3880
|
if (connectResolve) connectResolve();
|
|
@@ -3837,6 +4037,10 @@ var RealtimeAPI = class {
|
|
|
3837
4037
|
}
|
|
3838
4038
|
}
|
|
3839
4039
|
handleDisconnect() {
|
|
4040
|
+
if (this.sseFallbackActive) {
|
|
4041
|
+
this.ws = null;
|
|
4042
|
+
return;
|
|
4043
|
+
}
|
|
3840
4044
|
this.ws = null;
|
|
3841
4045
|
this._connectionId = null;
|
|
3842
4046
|
this.streamSessions.forEach((session) => {
|
|
@@ -3869,6 +4073,7 @@ var RealtimeAPI = class {
|
|
|
3869
4073
|
3e4
|
|
3870
4074
|
);
|
|
3871
4075
|
setTimeout(async () => {
|
|
4076
|
+
if (this.sseFallbackActive) return;
|
|
3872
4077
|
try {
|
|
3873
4078
|
await this.doConnect();
|
|
3874
4079
|
this.log("Reconnected successfully, restoring subscriptions...");
|
|
@@ -3879,12 +4084,19 @@ var RealtimeAPI = class {
|
|
|
3879
4084
|
);
|
|
3880
4085
|
} catch (e) {
|
|
3881
4086
|
this.logError("Reconnect failed", e);
|
|
4087
|
+
if (this.retryCount >= this.options.maxRetries && this.options.fallback !== "none" && !this.sseFallbackActive) {
|
|
4088
|
+
this.activateSseFallback(e);
|
|
4089
|
+
}
|
|
3882
4090
|
}
|
|
3883
4091
|
}, delay);
|
|
3884
4092
|
} else {
|
|
3885
|
-
this.
|
|
3886
|
-
|
|
3887
|
-
|
|
4093
|
+
if (this.options.fallback !== "none") {
|
|
4094
|
+
this.activateSseFallback(new Error("Connection lost. Max WS retries exceeded."));
|
|
4095
|
+
} else {
|
|
4096
|
+
this.state = "disconnected";
|
|
4097
|
+
this.notifyStateChange();
|
|
4098
|
+
this.notifyError(new Error("Connection lost. Max retries exceeded."));
|
|
4099
|
+
}
|
|
3888
4100
|
}
|
|
3889
4101
|
}
|
|
3890
4102
|
/**
|
|
@@ -3947,6 +4159,17 @@ var RealtimeAPI = class {
|
|
|
3947
4159
|
}
|
|
3948
4160
|
}
|
|
3949
4161
|
}
|
|
4162
|
+
/**
|
|
4163
|
+
* 양방향(WS 전용) 기능을 SSE fallback 모드에서 호출하면 명확한 에러를 던진다.
|
|
4164
|
+
* SSE 는 단방향이라 presence/typing/subscribe/sendMessage 를 지원하지 못한다.
|
|
4165
|
+
*/
|
|
4166
|
+
assertBidirectional(feature) {
|
|
4167
|
+
if (this.activeTransport === "sse") {
|
|
4168
|
+
throw new Error(
|
|
4169
|
+
`realtime.${feature}() requires a WebSocket connection, which is blocked on this network. Only AI streaming (realtime.stream) works in SSE fallback mode (cb.realtime.transport === 'sse').`
|
|
4170
|
+
);
|
|
4171
|
+
}
|
|
4172
|
+
}
|
|
3950
4173
|
sendRequest(message) {
|
|
3951
4174
|
return new Promise((resolve, reject) => {
|
|
3952
4175
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|