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/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: "include",
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: "include",
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
- if (this.state !== "connected") {
3139
- 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
+ }
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
- 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);
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.state = "disconnected";
3882
- this.notifyStateChange();
3883
- 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
+ }
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "connectbase-client",
3
- "version": "3.31.0",
3
+ "version": "3.33.0",
4
4
  "description": "Connect Base JavaScript/TypeScript SDK for browser and Node.js",
5
5
  "repository": {
6
6
  "type": "git",