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/dist/index.js CHANGED
@@ -2959,13 +2959,25 @@ var RealtimeAPI = class {
2959
2959
  this.state = "disconnected";
2960
2960
  this._connectionId = null;
2961
2961
  this._appId = null;
2962
+ /**
2963
+ * 현재 활성 transport.
2964
+ * - `'ws'`: WebSocket 정상 연결
2965
+ * - `'sse'`: WS 핸드셰이크 실패로 HTTP SSE fallback 모드 (AI `stream()` 만 지원)
2966
+ * - `null`: 미연결
2967
+ */
2968
+ this.activeTransport = null;
2969
+ /** SSE fallback 모드 활성 여부. true 면 백그라운드 WS 재연결 머신을 정지한다. */
2970
+ this.sseFallbackActive = false;
2971
+ /** SSE fallback 스트림의 AbortController (sessionId → controller) — stop()/disconnect() 에서 중단용 */
2972
+ this.sseSessions = /* @__PURE__ */ new Map();
2962
2973
  this.options = {
2963
2974
  maxRetries: 5,
2964
2975
  retryInterval: 1e3,
2965
2976
  userId: "",
2966
2977
  accessToken: "",
2967
2978
  timeout: 3e4,
2968
- debug: false
2979
+ debug: false,
2980
+ fallback: "sse"
2969
2981
  };
2970
2982
  this.retryCount = 0;
2971
2983
  this.pendingRequests = /* @__PURE__ */ new Map();
@@ -2994,6 +3006,13 @@ var RealtimeAPI = class {
2994
3006
  get appId() {
2995
3007
  return this._appId;
2996
3008
  }
3009
+ /**
3010
+ * 현재 활성 transport (`'ws'` | `'sse'` | `null`).
3011
+ * `'sse'` 이면 WS 가 차단되어 fallback 모드로 동작 중이며 AI `stream()` 만 가능하다.
3012
+ */
3013
+ get transport() {
3014
+ return this.activeTransport;
3015
+ }
2997
3016
  /**
2998
3017
  * WebSocket 연결
2999
3018
  * @param options 연결 옵션
@@ -3014,10 +3033,36 @@ var RealtimeAPI = class {
3014
3033
  this.connectPromise = this.doConnect();
3015
3034
  try {
3016
3035
  await this.connectPromise;
3036
+ } catch (e) {
3037
+ const hasAuth = !!(this.options.accessToken || this.http.getPublicKey());
3038
+ if (this.options.fallback !== "none" && hasAuth) {
3039
+ this.activateSseFallback(e);
3040
+ } else {
3041
+ throw e;
3042
+ }
3017
3043
  } finally {
3018
3044
  this.connectPromise = null;
3019
3045
  }
3020
3046
  }
3047
+ /**
3048
+ * SSE fallback 모드로 전환한다. WS 는 사용 불가하지만 state 는 'connected' 로 두어
3049
+ * (degraded) AI stream() 이 SSE 로 동작하게 한다. 백그라운드 WS 재연결 머신은 정지한다.
3050
+ */
3051
+ activateSseFallback(cause) {
3052
+ if (this.ws) {
3053
+ try {
3054
+ this.ws.close();
3055
+ } catch {
3056
+ }
3057
+ this.ws = null;
3058
+ }
3059
+ this.sseFallbackActive = true;
3060
+ this.activeTransport = "sse";
3061
+ this.state = "connected";
3062
+ this.retryCount = 0;
3063
+ this.log(`WS unavailable (${cause instanceof Error ? cause.message : String(cause)}); using SSE fallback transport for AI streaming`);
3064
+ this.notifyStateChange();
3065
+ }
3021
3066
  /**
3022
3067
  * 연결 해제
3023
3068
  */
@@ -3040,6 +3085,15 @@ var RealtimeAPI = class {
3040
3085
  }
3041
3086
  });
3042
3087
  this.streamSessions.clear();
3088
+ this.sseSessions.forEach((controller) => {
3089
+ try {
3090
+ controller.abort();
3091
+ } catch {
3092
+ }
3093
+ });
3094
+ this.sseSessions.clear();
3095
+ this.sseFallbackActive = false;
3096
+ this.activeTransport = null;
3043
3097
  this.presenceHandlers = [];
3044
3098
  this.presenceSubscriptions.clear();
3045
3099
  this.typingHandlers.clear();
@@ -3053,6 +3107,7 @@ var RealtimeAPI = class {
3053
3107
  * 카테고리 구독
3054
3108
  */
3055
3109
  async subscribe(category, options = {}) {
3110
+ this.assertBidirectional("subscribe");
3056
3111
  if (this.state !== "connected") {
3057
3112
  throw new Error("Not connected. Call connect() first.");
3058
3113
  }
@@ -3113,6 +3168,7 @@ var RealtimeAPI = class {
3113
3168
  * @param options 전송 옵션 (includeSelf: 발신자도 메시지 수신 여부, 기본값 true)
3114
3169
  */
3115
3170
  async sendMessage(category, data, options = {}) {
3171
+ this.assertBidirectional("sendMessage");
3116
3172
  if (this.state !== "connected") {
3117
3173
  throw new Error("Not connected");
3118
3174
  }
@@ -3130,6 +3186,7 @@ var RealtimeAPI = class {
3130
3186
  * 히스토리 조회
3131
3187
  */
3132
3188
  async getHistory(category, limit) {
3189
+ this.assertBidirectional("getHistory");
3133
3190
  if (this.state !== "connected") {
3134
3191
  throw new Error("Not connected");
3135
3192
  }
@@ -3185,9 +3242,22 @@ var RealtimeAPI = class {
3185
3242
  * ```
3186
3243
  */
3187
3244
  async stream(messages, handlers, options = {}) {
3188
- if (this.state !== "connected") {
3189
- throw new Error("Not connected. Call connect() first.");
3245
+ const canWs = this.state === "connected" && this.activeTransport === "ws" && this.ws?.readyState === WebSocket.OPEN;
3246
+ if (canWs) {
3247
+ try {
3248
+ return this.streamViaWS(messages, handlers, options);
3249
+ } catch (e) {
3250
+ if (this.options.fallback === "none") throw e;
3251
+ this.log(`WS stream send failed (${e instanceof Error ? e.message : String(e)}); falling back to SSE`);
3252
+ }
3253
+ }
3254
+ if (this.options.fallback !== "none") {
3255
+ return this.streamViaSSE(messages, handlers, options);
3190
3256
  }
3257
+ throw new Error("Not connected. Call connect() first.");
3258
+ }
3259
+ /** WebSocket 으로 AI 스트리밍 (기본 경로) */
3260
+ streamViaWS(messages, handlers, options) {
3191
3261
  const requestId = this.generateRequestId();
3192
3262
  const sessionId = options.sessionId || this.generateRequestId();
3193
3263
  this.streamSessions.set(sessionId, {
@@ -3222,11 +3292,130 @@ var RealtimeAPI = class {
3222
3292
  }
3223
3293
  };
3224
3294
  }
3295
+ /**
3296
+ * HTTP SSE (`/v1/public/ai/chat/stream`) 로 AI 스트리밍 — WS 가 차단된 네트워크용 fallback.
3297
+ *
3298
+ * SSE 는 단방향이라 WS 의 `StreamDoneData` 토큰 카운트(total/prompt)를 받지 못하므로
3299
+ * `totalTokens`/`promptTokens` 는 0 으로 채워진다(`fullText`/`duration` 은 정확). `mcpGroup`
3300
+ * (MCP 도구) 은 SSE fallback 에서 미지원 — 지정 시 무시되고 도구 없이 스트리밍한다.
3301
+ */
3302
+ streamViaSSE(messages, handlers, options) {
3303
+ const sessionId = options.sessionId || this.generateRequestId();
3304
+ const controller = new AbortController();
3305
+ this.sseSessions.set(sessionId, controller);
3306
+ if (options.mcpGroup) {
3307
+ this.log("mcpGroup is not supported in SSE fallback mode; streaming without MCP tools");
3308
+ }
3309
+ this.log(`Streaming via SSE fallback (session ${sessionId})`);
3310
+ void this.runSseStream(sessionId, messages, handlers, options, controller.signal);
3311
+ return {
3312
+ sessionId,
3313
+ stop: async () => {
3314
+ await this.stopStream(sessionId);
3315
+ }
3316
+ };
3317
+ }
3318
+ async runSseStream(sessionId, messages, handlers, options, signal) {
3319
+ const startedAt = Date.now();
3320
+ let tokenIndex = 0;
3321
+ let fullText = "";
3322
+ let reader;
3323
+ const wireMessages = options.system ? [{ role: "system", content: options.system }, ...messages] : messages;
3324
+ const body = {
3325
+ messages: wireMessages,
3326
+ provider: options.provider,
3327
+ model: options.model,
3328
+ temperature: options.temperature,
3329
+ maxTokens: options.maxTokens
3330
+ };
3331
+ try {
3332
+ const response = await this.http.fetchRaw("/v1/public/ai/chat/stream", {
3333
+ method: "POST",
3334
+ headers: { "Content-Type": "application/json" },
3335
+ body: JSON.stringify(body),
3336
+ signal
3337
+ });
3338
+ if (!response.ok) {
3339
+ const errData = await response.json().catch(() => ({ error: "Stream request failed" }));
3340
+ handlers.onError?.(new Error(errData.error || errData.message || "Stream request failed"));
3341
+ return;
3342
+ }
3343
+ reader = response.body?.getReader();
3344
+ if (!reader) {
3345
+ handlers.onError?.(new Error("ReadableStream not supported"));
3346
+ return;
3347
+ }
3348
+ const decoder = new TextDecoder();
3349
+ let buffer = "";
3350
+ while (true) {
3351
+ const { done, value } = await reader.read();
3352
+ if (done) break;
3353
+ buffer += decoder.decode(value, { stream: true });
3354
+ const lines = buffer.split("\n");
3355
+ buffer = lines.pop() || "";
3356
+ for (const line of lines) {
3357
+ if (!line.startsWith("data: ")) continue;
3358
+ const data = line.slice(6).trim();
3359
+ if (data === "[DONE]") {
3360
+ handlers.onDone?.({ sessionId, fullText, totalTokens: 0, promptTokens: 0, duration: Date.now() - startedAt });
3361
+ return;
3362
+ }
3363
+ try {
3364
+ const ev = JSON.parse(data);
3365
+ if (ev.error) {
3366
+ handlers.onError?.(new Error(ev.message || ev.error || "stream error"));
3367
+ return;
3368
+ }
3369
+ if (ev.type === "tool_start") {
3370
+ handlers.onToolCall?.(ev.name || "", ev.arguments || {}, tokenIndex);
3371
+ continue;
3372
+ }
3373
+ if (ev.type === "tool_end") {
3374
+ handlers.onToolResult?.(ev.name || "", ev.success !== false, ev.durationMs || 0, tokenIndex);
3375
+ continue;
3376
+ }
3377
+ if (ev.type === "sources" || ev.type === "searching" || ev.type === "heartbeat") {
3378
+ continue;
3379
+ }
3380
+ if (ev.content) {
3381
+ fullText += ev.content;
3382
+ handlers.onToken?.(ev.content, tokenIndex++);
3383
+ }
3384
+ if (ev.done) {
3385
+ handlers.onDone?.({ sessionId, fullText, totalTokens: 0, promptTokens: 0, duration: Date.now() - startedAt });
3386
+ return;
3387
+ }
3388
+ } catch {
3389
+ }
3390
+ }
3391
+ }
3392
+ } catch (err) {
3393
+ const aborted = signal.aborted || err instanceof DOMException && err.name === "AbortError" || typeof err === "object" && err !== null && err.name === "AbortError";
3394
+ if (!aborted) {
3395
+ handlers.onError?.(err instanceof Error ? err : new Error(String(err)));
3396
+ }
3397
+ } finally {
3398
+ this.sseSessions.delete(sessionId);
3399
+ if (reader) {
3400
+ try {
3401
+ await reader.cancel();
3402
+ } catch {
3403
+ }
3404
+ }
3405
+ }
3406
+ }
3225
3407
  /**
3226
3408
  * 스트리밍 중지
3227
3409
  */
3228
3410
  async stopStream(sessionId) {
3229
- if (this.state !== "connected") {
3411
+ const sse = this.sseSessions.get(sessionId);
3412
+ if (sse) {
3413
+ sse.abort();
3414
+ this.sseSessions.delete(sessionId);
3415
+ return;
3416
+ }
3417
+ if (this.state !== "connected" || !this.ws || this.ws.readyState !== WebSocket.OPEN) {
3418
+ this.streamSessions.delete(sessionId);
3230
3419
  return;
3231
3420
  }
3232
3421
  const requestId = this.generateRequestId();
@@ -3285,6 +3474,7 @@ var RealtimeAPI = class {
3285
3474
  * ```
3286
3475
  */
3287
3476
  async setPresence(status, options = {}) {
3477
+ this.assertBidirectional("setPresence");
3288
3478
  if (this.state !== "connected") {
3289
3479
  throw new Error("Not connected");
3290
3480
  }
@@ -3312,6 +3502,7 @@ var RealtimeAPI = class {
3312
3502
  * ```
3313
3503
  */
3314
3504
  async setPresenceOnDisconnect(status, metadata) {
3505
+ this.assertBidirectional("setPresenceOnDisconnect");
3315
3506
  if (this.state !== "connected") {
3316
3507
  throw new Error("Not connected");
3317
3508
  }
@@ -3335,6 +3526,7 @@ var RealtimeAPI = class {
3335
3526
  * ```
3336
3527
  */
3337
3528
  async getPresence(userId) {
3529
+ this.assertBidirectional("getPresence");
3338
3530
  if (this.state !== "connected") {
3339
3531
  throw new Error("Not connected");
3340
3532
  }
@@ -3365,6 +3557,7 @@ var RealtimeAPI = class {
3365
3557
  * ```
3366
3558
  */
3367
3559
  async getPresenceMany(userIds) {
3560
+ this.assertBidirectional("getPresenceMany");
3368
3561
  if (this.state !== "connected") {
3369
3562
  throw new Error("Not connected");
3370
3563
  }
@@ -3404,6 +3597,7 @@ var RealtimeAPI = class {
3404
3597
  * ```
3405
3598
  */
3406
3599
  async subscribePresence(userId, handler) {
3600
+ this.assertBidirectional("subscribePresence");
3407
3601
  if (this.state !== "connected") {
3408
3602
  throw new Error("Not connected");
3409
3603
  }
@@ -3473,6 +3667,7 @@ var RealtimeAPI = class {
3473
3667
  * ```
3474
3668
  */
3475
3669
  async startTyping(roomId) {
3670
+ this.assertBidirectional("startTyping");
3476
3671
  if (this.state !== "connected") {
3477
3672
  throw new Error("Not connected");
3478
3673
  }
@@ -3497,6 +3692,7 @@ var RealtimeAPI = class {
3497
3692
  * ```
3498
3693
  */
3499
3694
  async stopTyping(roomId) {
3695
+ this.assertBidirectional("stopTyping");
3500
3696
  if (this.state !== "connected") {
3501
3697
  throw new Error("Not connected");
3502
3698
  }
@@ -3526,6 +3722,7 @@ var RealtimeAPI = class {
3526
3722
  * ```
3527
3723
  */
3528
3724
  async onTypingChange(roomId, handler) {
3725
+ this.assertBidirectional("onTypingChange");
3529
3726
  if (this.state !== "connected") {
3530
3727
  throw new Error("Not connected");
3531
3728
  }
@@ -3573,6 +3770,7 @@ var RealtimeAPI = class {
3573
3770
  * ```
3574
3771
  */
3575
3772
  async markRead(category, messageIds) {
3773
+ this.assertBidirectional("markRead");
3576
3774
  if (this.state !== "connected") {
3577
3775
  throw new Error("Not connected");
3578
3776
  }
@@ -3721,6 +3919,8 @@ var RealtimeAPI = class {
3721
3919
  this._connectionId = data.connection_id;
3722
3920
  this._appId = data.app_id;
3723
3921
  this.state = "connected";
3922
+ this.activeTransport = "ws";
3923
+ this.sseFallbackActive = false;
3724
3924
  this.retryCount = 0;
3725
3925
  this.notifyStateChange();
3726
3926
  if (connectResolve) connectResolve();
@@ -3883,6 +4083,10 @@ var RealtimeAPI = class {
3883
4083
  }
3884
4084
  }
3885
4085
  handleDisconnect() {
4086
+ if (this.sseFallbackActive) {
4087
+ this.ws = null;
4088
+ return;
4089
+ }
3886
4090
  this.ws = null;
3887
4091
  this._connectionId = null;
3888
4092
  this.streamSessions.forEach((session) => {
@@ -3915,6 +4119,7 @@ var RealtimeAPI = class {
3915
4119
  3e4
3916
4120
  );
3917
4121
  setTimeout(async () => {
4122
+ if (this.sseFallbackActive) return;
3918
4123
  try {
3919
4124
  await this.doConnect();
3920
4125
  this.log("Reconnected successfully, restoring subscriptions...");
@@ -3925,12 +4130,19 @@ var RealtimeAPI = class {
3925
4130
  );
3926
4131
  } catch (e) {
3927
4132
  this.logError("Reconnect failed", e);
4133
+ if (this.retryCount >= this.options.maxRetries && this.options.fallback !== "none" && !this.sseFallbackActive) {
4134
+ this.activateSseFallback(e);
4135
+ }
3928
4136
  }
3929
4137
  }, delay);
3930
4138
  } else {
3931
- this.state = "disconnected";
3932
- this.notifyStateChange();
3933
- this.notifyError(new Error("Connection lost. Max retries exceeded."));
4139
+ if (this.options.fallback !== "none") {
4140
+ this.activateSseFallback(new Error("Connection lost. Max WS retries exceeded."));
4141
+ } else {
4142
+ this.state = "disconnected";
4143
+ this.notifyStateChange();
4144
+ this.notifyError(new Error("Connection lost. Max retries exceeded."));
4145
+ }
3934
4146
  }
3935
4147
  }
3936
4148
  /**
@@ -3993,6 +4205,17 @@ var RealtimeAPI = class {
3993
4205
  }
3994
4206
  }
3995
4207
  }
4208
+ /**
4209
+ * 양방향(WS 전용) 기능을 SSE fallback 모드에서 호출하면 명확한 에러를 던진다.
4210
+ * SSE 는 단방향이라 presence/typing/subscribe/sendMessage 를 지원하지 못한다.
4211
+ */
4212
+ assertBidirectional(feature) {
4213
+ if (this.activeTransport === "sse") {
4214
+ throw new Error(
4215
+ `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').`
4216
+ );
4217
+ }
4218
+ }
3996
4219
  sendRequest(message) {
3997
4220
  return new Promise((resolve, reject) => {
3998
4221
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {