connectbase-client 3.31.0 → 3.33.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 +55 -0
- package/dist/connect-base.umd.js +5 -4
- package/dist/index.d.mts +67 -0
- package/dist/index.d.ts +67 -0
- package/dist/index.js +236 -9
- package/dist/index.mjs +236 -9
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -107,6 +107,10 @@ function sanitizePathForBreadcrumb(rawUrl) {
|
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
// src/core/http.ts
|
|
110
|
+
function fetchCredentialsForPath(url) {
|
|
111
|
+
const path = url.split("?")[0];
|
|
112
|
+
return path.startsWith("/v1/public/") ? "omit" : "include";
|
|
113
|
+
}
|
|
110
114
|
var TOKEN_STORAGE_KEY = "cb_auth_tokens";
|
|
111
115
|
function decodeJwtPayload(token) {
|
|
112
116
|
try {
|
|
@@ -633,7 +637,7 @@ var HttpClient = class {
|
|
|
633
637
|
try {
|
|
634
638
|
const response = await fetch(`${this.config.baseUrl}${url}`, {
|
|
635
639
|
...init,
|
|
636
|
-
credentials:
|
|
640
|
+
credentials: fetchCredentialsForPath(url),
|
|
637
641
|
signal
|
|
638
642
|
});
|
|
639
643
|
return { response, ok: response.ok, status: response.status };
|
|
@@ -702,7 +706,7 @@ var HttpClient = class {
|
|
|
702
706
|
}
|
|
703
707
|
return fetch(`${this.config.baseUrl}${url}`, {
|
|
704
708
|
...init,
|
|
705
|
-
credentials:
|
|
709
|
+
credentials: fetchCredentialsForPath(url),
|
|
706
710
|
headers: mergedHeaders
|
|
707
711
|
});
|
|
708
712
|
}
|
|
@@ -2909,13 +2913,25 @@ var RealtimeAPI = class {
|
|
|
2909
2913
|
this.state = "disconnected";
|
|
2910
2914
|
this._connectionId = null;
|
|
2911
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();
|
|
2912
2927
|
this.options = {
|
|
2913
2928
|
maxRetries: 5,
|
|
2914
2929
|
retryInterval: 1e3,
|
|
2915
2930
|
userId: "",
|
|
2916
2931
|
accessToken: "",
|
|
2917
2932
|
timeout: 3e4,
|
|
2918
|
-
debug: false
|
|
2933
|
+
debug: false,
|
|
2934
|
+
fallback: "sse"
|
|
2919
2935
|
};
|
|
2920
2936
|
this.retryCount = 0;
|
|
2921
2937
|
this.pendingRequests = /* @__PURE__ */ new Map();
|
|
@@ -2944,6 +2960,13 @@ var RealtimeAPI = class {
|
|
|
2944
2960
|
get appId() {
|
|
2945
2961
|
return this._appId;
|
|
2946
2962
|
}
|
|
2963
|
+
/**
|
|
2964
|
+
* 현재 활성 transport (`'ws'` | `'sse'` | `null`).
|
|
2965
|
+
* `'sse'` 이면 WS 가 차단되어 fallback 모드로 동작 중이며 AI `stream()` 만 가능하다.
|
|
2966
|
+
*/
|
|
2967
|
+
get transport() {
|
|
2968
|
+
return this.activeTransport;
|
|
2969
|
+
}
|
|
2947
2970
|
/**
|
|
2948
2971
|
* WebSocket 연결
|
|
2949
2972
|
* @param options 연결 옵션
|
|
@@ -2964,10 +2987,36 @@ var RealtimeAPI = class {
|
|
|
2964
2987
|
this.connectPromise = this.doConnect();
|
|
2965
2988
|
try {
|
|
2966
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
|
+
}
|
|
2967
2997
|
} finally {
|
|
2968
2998
|
this.connectPromise = null;
|
|
2969
2999
|
}
|
|
2970
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
|
+
}
|
|
2971
3020
|
/**
|
|
2972
3021
|
* 연결 해제
|
|
2973
3022
|
*/
|
|
@@ -2990,6 +3039,15 @@ var RealtimeAPI = class {
|
|
|
2990
3039
|
}
|
|
2991
3040
|
});
|
|
2992
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;
|
|
2993
3051
|
this.presenceHandlers = [];
|
|
2994
3052
|
this.presenceSubscriptions.clear();
|
|
2995
3053
|
this.typingHandlers.clear();
|
|
@@ -3003,6 +3061,7 @@ var RealtimeAPI = class {
|
|
|
3003
3061
|
* 카테고리 구독
|
|
3004
3062
|
*/
|
|
3005
3063
|
async subscribe(category, options = {}) {
|
|
3064
|
+
this.assertBidirectional("subscribe");
|
|
3006
3065
|
if (this.state !== "connected") {
|
|
3007
3066
|
throw new Error("Not connected. Call connect() first.");
|
|
3008
3067
|
}
|
|
@@ -3063,6 +3122,7 @@ var RealtimeAPI = class {
|
|
|
3063
3122
|
* @param options 전송 옵션 (includeSelf: 발신자도 메시지 수신 여부, 기본값 true)
|
|
3064
3123
|
*/
|
|
3065
3124
|
async sendMessage(category, data, options = {}) {
|
|
3125
|
+
this.assertBidirectional("sendMessage");
|
|
3066
3126
|
if (this.state !== "connected") {
|
|
3067
3127
|
throw new Error("Not connected");
|
|
3068
3128
|
}
|
|
@@ -3080,6 +3140,7 @@ var RealtimeAPI = class {
|
|
|
3080
3140
|
* 히스토리 조회
|
|
3081
3141
|
*/
|
|
3082
3142
|
async getHistory(category, limit) {
|
|
3143
|
+
this.assertBidirectional("getHistory");
|
|
3083
3144
|
if (this.state !== "connected") {
|
|
3084
3145
|
throw new Error("Not connected");
|
|
3085
3146
|
}
|
|
@@ -3135,9 +3196,22 @@ var RealtimeAPI = class {
|
|
|
3135
3196
|
* ```
|
|
3136
3197
|
*/
|
|
3137
3198
|
async stream(messages, handlers, options = {}) {
|
|
3138
|
-
|
|
3139
|
-
|
|
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
|
+
}
|
|
3140
3207
|
}
|
|
3208
|
+
if (this.options.fallback !== "none") {
|
|
3209
|
+
return this.streamViaSSE(messages, handlers, options);
|
|
3210
|
+
}
|
|
3211
|
+
throw new Error("Not connected. Call connect() first.");
|
|
3212
|
+
}
|
|
3213
|
+
/** WebSocket 으로 AI 스트리밍 (기본 경로) */
|
|
3214
|
+
streamViaWS(messages, handlers, options) {
|
|
3141
3215
|
const requestId = this.generateRequestId();
|
|
3142
3216
|
const sessionId = options.sessionId || this.generateRequestId();
|
|
3143
3217
|
this.streamSessions.set(sessionId, {
|
|
@@ -3172,11 +3246,130 @@ var RealtimeAPI = class {
|
|
|
3172
3246
|
}
|
|
3173
3247
|
};
|
|
3174
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
|
+
}
|
|
3175
3361
|
/**
|
|
3176
3362
|
* 스트리밍 중지
|
|
3177
3363
|
*/
|
|
3178
3364
|
async stopStream(sessionId) {
|
|
3179
|
-
|
|
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);
|
|
3180
3373
|
return;
|
|
3181
3374
|
}
|
|
3182
3375
|
const requestId = this.generateRequestId();
|
|
@@ -3235,6 +3428,7 @@ var RealtimeAPI = class {
|
|
|
3235
3428
|
* ```
|
|
3236
3429
|
*/
|
|
3237
3430
|
async setPresence(status, options = {}) {
|
|
3431
|
+
this.assertBidirectional("setPresence");
|
|
3238
3432
|
if (this.state !== "connected") {
|
|
3239
3433
|
throw new Error("Not connected");
|
|
3240
3434
|
}
|
|
@@ -3262,6 +3456,7 @@ var RealtimeAPI = class {
|
|
|
3262
3456
|
* ```
|
|
3263
3457
|
*/
|
|
3264
3458
|
async setPresenceOnDisconnect(status, metadata) {
|
|
3459
|
+
this.assertBidirectional("setPresenceOnDisconnect");
|
|
3265
3460
|
if (this.state !== "connected") {
|
|
3266
3461
|
throw new Error("Not connected");
|
|
3267
3462
|
}
|
|
@@ -3285,6 +3480,7 @@ var RealtimeAPI = class {
|
|
|
3285
3480
|
* ```
|
|
3286
3481
|
*/
|
|
3287
3482
|
async getPresence(userId) {
|
|
3483
|
+
this.assertBidirectional("getPresence");
|
|
3288
3484
|
if (this.state !== "connected") {
|
|
3289
3485
|
throw new Error("Not connected");
|
|
3290
3486
|
}
|
|
@@ -3315,6 +3511,7 @@ var RealtimeAPI = class {
|
|
|
3315
3511
|
* ```
|
|
3316
3512
|
*/
|
|
3317
3513
|
async getPresenceMany(userIds) {
|
|
3514
|
+
this.assertBidirectional("getPresenceMany");
|
|
3318
3515
|
if (this.state !== "connected") {
|
|
3319
3516
|
throw new Error("Not connected");
|
|
3320
3517
|
}
|
|
@@ -3354,6 +3551,7 @@ var RealtimeAPI = class {
|
|
|
3354
3551
|
* ```
|
|
3355
3552
|
*/
|
|
3356
3553
|
async subscribePresence(userId, handler) {
|
|
3554
|
+
this.assertBidirectional("subscribePresence");
|
|
3357
3555
|
if (this.state !== "connected") {
|
|
3358
3556
|
throw new Error("Not connected");
|
|
3359
3557
|
}
|
|
@@ -3423,6 +3621,7 @@ var RealtimeAPI = class {
|
|
|
3423
3621
|
* ```
|
|
3424
3622
|
*/
|
|
3425
3623
|
async startTyping(roomId) {
|
|
3624
|
+
this.assertBidirectional("startTyping");
|
|
3426
3625
|
if (this.state !== "connected") {
|
|
3427
3626
|
throw new Error("Not connected");
|
|
3428
3627
|
}
|
|
@@ -3447,6 +3646,7 @@ var RealtimeAPI = class {
|
|
|
3447
3646
|
* ```
|
|
3448
3647
|
*/
|
|
3449
3648
|
async stopTyping(roomId) {
|
|
3649
|
+
this.assertBidirectional("stopTyping");
|
|
3450
3650
|
if (this.state !== "connected") {
|
|
3451
3651
|
throw new Error("Not connected");
|
|
3452
3652
|
}
|
|
@@ -3476,6 +3676,7 @@ var RealtimeAPI = class {
|
|
|
3476
3676
|
* ```
|
|
3477
3677
|
*/
|
|
3478
3678
|
async onTypingChange(roomId, handler) {
|
|
3679
|
+
this.assertBidirectional("onTypingChange");
|
|
3479
3680
|
if (this.state !== "connected") {
|
|
3480
3681
|
throw new Error("Not connected");
|
|
3481
3682
|
}
|
|
@@ -3523,6 +3724,7 @@ var RealtimeAPI = class {
|
|
|
3523
3724
|
* ```
|
|
3524
3725
|
*/
|
|
3525
3726
|
async markRead(category, messageIds) {
|
|
3727
|
+
this.assertBidirectional("markRead");
|
|
3526
3728
|
if (this.state !== "connected") {
|
|
3527
3729
|
throw new Error("Not connected");
|
|
3528
3730
|
}
|
|
@@ -3671,6 +3873,8 @@ var RealtimeAPI = class {
|
|
|
3671
3873
|
this._connectionId = data.connection_id;
|
|
3672
3874
|
this._appId = data.app_id;
|
|
3673
3875
|
this.state = "connected";
|
|
3876
|
+
this.activeTransport = "ws";
|
|
3877
|
+
this.sseFallbackActive = false;
|
|
3674
3878
|
this.retryCount = 0;
|
|
3675
3879
|
this.notifyStateChange();
|
|
3676
3880
|
if (connectResolve) connectResolve();
|
|
@@ -3833,6 +4037,10 @@ var RealtimeAPI = class {
|
|
|
3833
4037
|
}
|
|
3834
4038
|
}
|
|
3835
4039
|
handleDisconnect() {
|
|
4040
|
+
if (this.sseFallbackActive) {
|
|
4041
|
+
this.ws = null;
|
|
4042
|
+
return;
|
|
4043
|
+
}
|
|
3836
4044
|
this.ws = null;
|
|
3837
4045
|
this._connectionId = null;
|
|
3838
4046
|
this.streamSessions.forEach((session) => {
|
|
@@ -3865,6 +4073,7 @@ var RealtimeAPI = class {
|
|
|
3865
4073
|
3e4
|
|
3866
4074
|
);
|
|
3867
4075
|
setTimeout(async () => {
|
|
4076
|
+
if (this.sseFallbackActive) return;
|
|
3868
4077
|
try {
|
|
3869
4078
|
await this.doConnect();
|
|
3870
4079
|
this.log("Reconnected successfully, restoring subscriptions...");
|
|
@@ -3875,12 +4084,19 @@ var RealtimeAPI = class {
|
|
|
3875
4084
|
);
|
|
3876
4085
|
} catch (e) {
|
|
3877
4086
|
this.logError("Reconnect failed", e);
|
|
4087
|
+
if (this.retryCount >= this.options.maxRetries && this.options.fallback !== "none" && !this.sseFallbackActive) {
|
|
4088
|
+
this.activateSseFallback(e);
|
|
4089
|
+
}
|
|
3878
4090
|
}
|
|
3879
4091
|
}, delay);
|
|
3880
4092
|
} else {
|
|
3881
|
-
this.
|
|
3882
|
-
|
|
3883
|
-
|
|
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
|
+
}
|
|
3884
4100
|
}
|
|
3885
4101
|
}
|
|
3886
4102
|
/**
|
|
@@ -3943,6 +4159,17 @@ var RealtimeAPI = class {
|
|
|
3943
4159
|
}
|
|
3944
4160
|
}
|
|
3945
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
|
+
}
|
|
3946
4173
|
sendRequest(message) {
|
|
3947
4174
|
return new Promise((resolve, reject) => {
|
|
3948
4175
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|