connectbase-client 6.4.0 → 6.6.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 CHANGED
@@ -3,6 +3,112 @@
3
3
  본 SDK 의 모든 주요 변경사항을 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/) 형식으로 기록합니다.
4
4
  버전은 [Semantic Versioning](https://semver.org/lang/ko/) 을 따릅니다.
5
5
 
6
+ ## [6.6.0] - 2026-10-21
7
+
8
+ ### Added — 카테고리 발행 정책: 방송인과 "위로만" 가는 메시지
9
+
10
+ 1명이 방송하고 많은 사람이 받는 카테고리(가상 콘서트, 라이브 방송)를 위해 카테고리에 방송인 역할(`publisher_roles`)과
11
+ 발행 정책(`send_policy`)을 둘 수 있습니다(콘솔, API, MCP 도구). 방송인은 그 역할을 가진 **로그인 멤버 연결**입니다.
12
+
13
+ - `open`(기본): 누가 보내든 구독자 모두가 받습니다 (지금과 같음).
14
+ - `upstream`: 방송인의 메시지는 모두에게, 방송인이 아닌 구독자의 메시지는 방송인에게만 갑니다. 관객 1,000명의 반응이
15
+ 관객 1,000명에게 복사되지 않습니다.
16
+ - `publishers_only`: 방송인만 보낼 수 있습니다 (관객이 보내면 `SEND_NOT_ALLOWED`).
17
+
18
+ SDK 쪽 추가:
19
+ - `subscription.info.isPublisher`, `subscription.info.sendPolicy`
20
+ - `message.fromPublisher` — 보낸 쪽이 방송인이라는 **서버 보증** (클라이언트가 위조할 수 없음)
21
+ - `send(data, { to: 'publishers' })` — 정책이 `open` 이어도 이 메시지만 방송인에게 (채팅은 모두에게, 반응은 위로)
22
+
23
+ ```ts
24
+ const concert = await cb.realtime.subscribe('concert')
25
+ concert.onMessage((m) => {
26
+ if (m.fromPublisher) renderStage(m.data) // 방송인 화면
27
+ else countReaction(m.data) // 방송인만 받는 관객 반응
28
+ })
29
+ await concert.send({ cheer: 1 }) // upstream 이면 방송인에게만
30
+ ```
31
+
32
+ ### Added — 요청 거부 코드: `RealtimeRequestError`
33
+
34
+ `subscribe`/`send`/`getHistory` 등을 서버가 거부하면 서버 오류 코드(`SEND_NOT_ALLOWED`, `RATE_LIMITED`,
35
+ `MESSAGE_TOO_LARGE`, `NOT_SUBSCRIBED` 등)를 담은 `RealtimeRequestError` 를 던집니다. 예전에는 코드를 버리고
36
+ 메시지만 담은 `Error` 였습니다. `Error` 를 상속하므로 기존 `catch` 코드는 그대로 동작합니다.
37
+
38
+ ### Changed — 연결이 몰릴 때 SSE 로 넘어가지 않고 서버가 알려 준 시간 뒤 스스로 다시 붙습니다
39
+
40
+ 한 IP 에서 연결을 짧은 시간에 많이 열면 서버가 연결 속도 제한으로 거부합니다. 예전에는 이 거부가 업그레이드 전 HTTP
41
+ 429 라 브라우저가 이유를 못 읽었고, SDK 가 "네트워크가 WebSocket 을 막았다" 로 오인해 SSE 로 넘어갔습니다. 이제 서버가
42
+ `rate_limited` 와 재시도 시각(`retryAfterMs`)을 알려 주고, `connect()` 는 연결 제한 시간(`timeout`) 안에서 그 시각에
43
+ 지터를 더해 스스로 다시 붙습니다. 시간 안에 못 붙으면 `RealtimeConnectionError`(`code: 'rate_limited'`, `retryAfterMs`)를
44
+ 던집니다.
45
+
46
+ 이유 없는 핸드셰이크 실패도 한 번에 SSE 로 넘어가지 않고 두 번 더 시도합니다 — 네트워크가 WebSocket 을 실제로 막으면
47
+ 매번 실패하고, 일시적인 거부는 다시 하면 붙습니다. 자동 재연결 대기에는 지터를 더해(백오프의 절반에서 전체 사이)
48
+ 서버 재시작 뒤 모든 클라이언트가 같은 순간에 다시 몰리지 않게 했습니다.
49
+
50
+ ### Fixed — Node 에서 `connect()` 나 HTTP 요청이 끝나지 않던 문제
51
+
52
+ Node 의 fetch(undici)는 같은 원본으로 요청이 수백 개 몰릴 때 드물게 시간 제한의 abort 를 무시하고 영원히 대기합니다
53
+ (실측: 동시 400개 중 2개). SDK 는 시간 제한을 abort 에만 맡겨, 그 요청과 그것을 기다리는 `realtime.connect()` 가 끝나지
54
+ 않았습니다. 이제 시간 제한이 지나면 fetch 의 응답을 기다리지 않고 SDK 가 먼저 끝냅니다(HTTP 요청, 토큰 재발급, 원본 힌트).
55
+
56
+ ### Fixed — 브라우저도 첫 연결부터 저지연 원본(ws-direct)을 씁니다
57
+
58
+ 6.5.0 의 원본 힌트 조회(`/v1/public/edge-hints`)는 공개 키 헤더를 붙여 보내 브라우저가 CORS 사전 요청을 먼저 보냈고
59
+ (Cloudflare 왕복 두 번), 앱의 "로컬 개발 origin 허용" 이 꺼진 localhost 페이지에서는 막혔습니다. 그래서 Node 는 direct 로
60
+ 붙고 브라우저는 Cloudflare 경로로 붙었습니다. 이제 헤더 없는 단순 GET 으로 묻고 본문으로 배웁니다(서버도 키 없이 답합니다).
61
+
62
+ ## [6.5.0] - 2026-10-21
63
+
64
+ ### Added — 실시간 연결 거부 사유: `RealtimeConnectionError`
65
+
66
+ 서버가 실시간 연결을 받지 않으면 `cb.realtime.connect()` 가 이유를 담은 `RealtimeConnectionError` 를 던집니다.
67
+ `code`(`connection_limit_exceeded`, `quota_exceeded`, `server_at_capacity`, `auth_unavailable`, `unauthorized`,
68
+ `forbidden`, `invalid_user_id`, `internal_error`), `retryable`, `closeCode`(4000 + HTTP 상태), `details`
69
+ (예: 동시접속 초과의 `current`/`limit`)가 들어 있습니다.
70
+
71
+ 브라우저의 WebSocket API 는 실패한 핸드셰이크의 HTTP 상태를 읽지 못해, 지금까지는 동시접속 초과도 인증 실패도
72
+ "Connection timeout"/"Failed to connect" 로만 보였습니다. SDK 가 `reject_mode=close` 로 협상하면 서버가 연결을
73
+ 받은 뒤 이유를 보내고 4xxx 로 닫습니다(서버가 구버전이면 예전과 같게 동작합니다).
74
+
75
+ ```ts
76
+ try {
77
+ await cb.realtime.connect()
78
+ } catch (e) {
79
+ if (e instanceof RealtimeConnectionError && e.code === 'connection_limit_exceeded') {
80
+ showFull(e.details?.limit)
81
+ }
82
+ }
83
+ ```
84
+
85
+ ### Changed — 서버가 거부한 연결은 SSE 폴백으로 넘어가지 않습니다
86
+
87
+ 예전에는 거부도 네트워크 실패처럼 처리돼 SSE degraded 모드로 넘어갔고, `connect()` 는 성공했는데 `subscribe`/
88
+ `send` 가 나중에 이유 없이 실패했습니다. 이제 거부는 위 에러로 던집니다. 네트워크/프록시가 WebSocket 을 막는
89
+ 경우(사유 없는 실패)는 종전처럼 SSE 로 넘어갑니다.
90
+
91
+ 자동 재연결 중에 거부되면 `retryable` 이면 지수 백오프로 계속 시도하고, 아니면(`unauthorized`, `quota_exceeded`
92
+ 등) 재연결을 멈추고 `onError` 로 같은 에러를 보냅니다.
93
+
94
+ ### Changed — 첫 연결 전에 저지연 원본을 묻습니다
95
+
96
+ realtime 만 쓰는 페이지는 WS 를 열기 전에 REST 를 부르지 않아 direct 원본(`ws-direct`)을 배우지 못했고,
97
+ 첫 연결이 늘 Cloudflare 를 거쳐 한국 사용자는 미국(LAX)을 도는 ~270ms 로 붙었습니다. 이제 direct 원본을 아직
98
+ 모르면 `connect()` 가 `GET /v1/public/edge-hints` 를 한 번(최대 1.5초) 묻고 연결합니다. 해외 사용자와 킬 스위치가
99
+ 켜진 경우엔 빈 응답이라 원래 소켓 주소로 갑니다. 세션당 한 번만 묻습니다.
100
+
101
+ ### Fixed — 재연결 시도가 한 번이라도 실패하면 구독이 복구되지 않던 문제
102
+
103
+ 연결이 끊긴 뒤 첫 재연결 시도가 실패하면(서버 롤링 재시작 등) 다음 시도에서 연결에 성공해도 카테고리 구독,
104
+ 프레전스 구독, 타이핑 핸들러가 하나도 복구되지 않았습니다. 실패한 시도마다 이미 비워진 구독 맵으로 복구
105
+ 스냅샷을 덮었기 때문입니다. 이제 끊긴 순간의 스냅샷을 재연결에 성공할 때까지 유지합니다.
106
+
107
+ ### Fixed — 재연결 대기 중 `disconnect()` 해도 다시 연결되던 문제
108
+
109
+ 백오프 대기 중에 `disconnect()` 를 부르면 예약된 재연결이 그대로 실행돼 다시 연결됐습니다. 이제 대기가 끝났을 때
110
+ 상태가 재연결 중이 아니면 연결하지 않습니다.
111
+
6
112
  ## [6.4.0] - 2026-10-01
7
113
 
8
114
  ### Added — 웹 배포에 브라우저 ML 모델 확장자 허용 (`.onnx` `.tflite` `.task` `.safetensors`)
package/README.md CHANGED
@@ -1006,6 +1006,68 @@ cb.realtime.transport // 'ws' | 'sse' | null
1006
1006
  > **v5 → v6 BREAKING**: v5 까지 `isConnected()` 는 SSE 폴백에서도 true 였습니다.
1007
1007
  > 스트리밍 게이팅에 쓰고 있었다면 `isStreamReady()` 로 바꾸세요.
1008
1008
 
1009
+ #### Connection rejected (v6.5.0)
1010
+
1011
+ 서버가 연결을 받지 않으면(동시접속 한도, 할당량, 인증 실패 등) `connect()` 가 `RealtimeConnectionError` 를
1012
+ 던집니다. 이 경우는 SSE 폴백으로 넘어가지 않습니다 — 네트워크는 정상이고 서버가 거부한 것이기 때문입니다.
1013
+
1014
+ ```typescript
1015
+ import { RealtimeConnectionError } from 'connectbase-client'
1016
+
1017
+ try {
1018
+ await cb.realtime.connect()
1019
+ } catch (e) {
1020
+ if (e instanceof RealtimeConnectionError) {
1021
+ e.code // 'connection_limit_exceeded' | 'quota_exceeded' | 'server_at_capacity' | 'unauthorized' | ...
1022
+ e.retryable // 나중에 다시 연결하면 성공할 수 있는지
1023
+ e.details // 예: { current: 100, limit: 100 }
1024
+ }
1025
+ }
1026
+ ```
1027
+
1028
+ 자동 재연결 중의 거부는 `retryable` 이면 백오프로 계속 시도하고, 아니면 재연결을 멈추고 `onError` 로 같은 에러를
1029
+ 보냅니다. 서버 코드(함수)에서 realtime 을 쓴다면 끝날 때 `cb.realtime.disconnect()` 를 부르세요 — 함수 컨테이너는
1030
+ 호출 사이에 재사용되므로 닫지 않은 소켓이 호출마다 쌓입니다(`unsubscribe()` 는 소켓을 닫지 않습니다).
1031
+
1032
+ 한 IP 에서 연결이 몰려 서버가 `rate_limited` 로 거부하면(v6.6.0+) `connect()` 가 서버가 알려 준 시간(`retryAfterMs`)에
1033
+ 지터를 더해 스스로 다시 붙습니다. 연결 제한 시간(`timeout`, 기본 30초) 안에 못 붙을 때만 이 에러를 던집니다.
1034
+
1035
+ #### Broadcast categories: publishers (v6.6.0)
1036
+
1037
+ 1명이 방송하고 많은 사람이 받는 카테고리는 콘솔에서 **방송인 역할**(`publisher_roles`)과 **발행 정책**(`send_policy`)을
1038
+ 설정합니다. 방송인은 그 역할을 가진 로그인 멤버(`accessToken`) 연결이고, 공개 키(익명) 연결은 방송인이 될 수 없습니다.
1039
+
1040
+ | `send_policy` | 방송인의 메시지 | 방송인이 아닌 구독자의 메시지 |
1041
+ |---|---|---|
1042
+ | `open` (기본) | 모두에게 | 모두에게 |
1043
+ | `upstream` | 모두에게 | **방송인에게만** (히스토리에 저장되지 않음) |
1044
+ | `publishers_only` | 모두에게 | 보낼 수 없음 (`SEND_NOT_ALLOWED`) |
1045
+
1046
+ ```typescript
1047
+ import { RealtimeRequestError } from 'connectbase-client'
1048
+
1049
+ const concert = await cb.realtime.subscribe('concert')
1050
+ concert.info.isPublisher // 이 연결이 방송인인가 (구독할 때 정해짐)
1051
+ concert.info.sendPolicy // 'open' | 'upstream' | 'publishers_only'
1052
+
1053
+ concert.onMessage((m) => {
1054
+ if (m.fromPublisher) renderStage(m.data) // 서버가 보증하는 방송인 메시지 (위조 불가)
1055
+ else countReaction(m.data) // upstream: 방송인만 받는 관객 반응
1056
+ })
1057
+
1058
+ await concert.send({ cheer: 1 }) // upstream 이면 방송인에게만
1059
+ await concert.send({ cheer: 1 }, { to: 'publishers' }) // open 카테고리에서도 이 메시지만 방송인에게
1060
+
1061
+ try {
1062
+ await concert.send({ x: 1 })
1063
+ } catch (e) {
1064
+ if (e instanceof RealtimeRequestError && e.code === 'SEND_NOT_ALLOWED') hideSendButton()
1065
+ }
1066
+ ```
1067
+
1068
+ 방송인 여부는 구독할 때 정해집니다. 멤버 역할이나 카테고리 설정을 바꾸면 다시 구독(또는 재연결)할 때 반영되고,
1069
+ 카테고리 설정은 서버 캐시 때문에 최대 5분 뒤에 반영됩니다.
1070
+
1009
1071
  #### Presence / Typing
1010
1072
 
1011
1073
  Presence(온라인 상태) 와 typing(입력 중 표시) 은 `cb.realtime.*` 가 단일 SoT 입니다.
package/dist/cli.js CHANGED
@@ -156,7 +156,7 @@ function handleTunnelError(msg, appId, localPort) {
156
156
  }
157
157
 
158
158
  // src/cli.ts
159
- var VERSION = "6.4.0" ? "6.4.0" : getPackageVersion();
159
+ var VERSION = "6.6.0" ? "6.6.0" : getPackageVersion();
160
160
  var DEFAULT_BASE_URL = "https://api.connectbase.world";
161
161
  var colors = {
162
162
  reset: "\x1B[0m",