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.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
- if (this.state !== "connected") {
3143
- throw new Error("Not connected. Call connect() first.");
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
- if (this.state !== "connected") {
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.state = "disconnected";
3886
- this.notifyStateChange();
3887
- this.notifyError(new Error("Connection lost. Max retries exceeded."));
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "connectbase-client",
3
- "version": "3.32.0",
3
+ "version": "3.34.0",
4
4
  "description": "Connect Base JavaScript/TypeScript SDK for browser and Node.js",
5
5
  "repository": {
6
6
  "type": "git",