connectbase-client 6.6.1 → 6.7.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 +81 -0
- package/README.md +46 -3
- package/dist/cli.js +1 -1
- package/dist/connect-base.umd.js +5 -5
- package/dist/index.d.mts +95 -15
- package/dist/index.d.ts +95 -15
- package/dist/index.js +145 -34
- package/dist/index.mjs +145 -34
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -161,7 +161,7 @@ declare class GameError extends Error {
|
|
|
161
161
|
* | `quota_exceeded` | 월간 WebSocket 사용 시간 소진 | 불가 | 플랜 업그레이드 전까지 재시도 무의미 |
|
|
162
162
|
* | `server_at_capacity` | 서버 일시 혼잡 | 가능 | 잠시 후 재시도 |
|
|
163
163
|
* | `rate_limited` | 한 IP 에서 연결 시도가 너무 잦음 (6.6.0+) | 가능 | `connect()` 가 `retryAfterMs` 만큼(지터 포함) 기다렸다 스스로 다시 붙는다. 연결 제한 시간 안에 못 붙으면 이 에러를 던진다 |
|
|
164
|
-
* | `auth_unavailable` | 인증 서버 일시 장애 | 가능 |
|
|
164
|
+
* | `auth_unavailable` | 인증 서버 일시 장애 | 가능 | `connect()` 가 `retryAfterMs` 만큼(없으면 지수 백오프) 기다렸다 스스로 다시 붙는다 (6.7.0+). 연결 제한 시간 안에 못 붙으면 이 에러를 던진다 |
|
|
165
165
|
* | `unauthorized` | 공개 키/액세스 토큰이 유효하지 않음 | 불가 | 키·토큰 확인 (토큰 만료면 새 토큰으로 `connect()`) |
|
|
166
166
|
* | `forbidden` | 다른 앱의 경로로 접속 | 불가 | 설정 확인 |
|
|
167
167
|
* | `invalid_user_id` | 공개 키 연결에 UUID 형식 `userId` | 불가 | `accessToken` 을 쓰거나 UUID 가 아닌 식별자 사용 |
|
|
@@ -177,8 +177,10 @@ type RealtimeConnectionErrorCode = "connection_limit_exceeded" | "quota_exceeded
|
|
|
177
177
|
* 연결을 받은 뒤 거부 이유를 보내고 4000+HTTP상태 close 코드(예: 4401, 4402, 4503)로 닫는다.
|
|
178
178
|
*
|
|
179
179
|
* - 거부는 SSE 폴백 대상이 아니다 — 네트워크는 정상이고 서버가 받지 않은 것이다.
|
|
180
|
-
* - 서버가 재시도 시각을 알려 준 거부(`retryAfterMs`, 예: `rate_limited`)는 `connect()` 가
|
|
181
|
-
* 시간(`timeout`) 안에서 그 시각에 지터를 더해 스스로 다시 시도한다.
|
|
180
|
+
* - 서버가 재시도 시각을 알려 준 거부(`retryAfterMs`, 예: `rate_limited`, `auth_unavailable`)는 `connect()` 가
|
|
181
|
+
* 연결 제한 시간(`timeout`) 안에서 그 시각에 지터를 더해 스스로 다시 시도한다. `rate_limited` 와
|
|
182
|
+
* `auth_unavailable` 은 시각이 없어도(구버전 서버) 지수 백오프로 다시 시도한다(6.7.0+). 그 밖의 거부
|
|
183
|
+
* (`unauthorized`, `connection_limit_exceeded` 등)는 바로 던진다.
|
|
182
184
|
* - 자동 재연결 중에 받은 거부는 `retryable` 이면 백오프로 계속 시도하고, 아니면 재연결을 멈추고
|
|
183
185
|
* `cb.realtime.onError` 로 이 에러를 알린다.
|
|
184
186
|
*
|
|
@@ -204,8 +206,8 @@ declare class RealtimeConnectionError extends Error {
|
|
|
204
206
|
/** 거부 상세 (예: connection_limit_exceeded 의 `current`/`limit`, quota_exceeded 의 `quota`/`unit`). */
|
|
205
207
|
details?: Record<string, unknown>;
|
|
206
208
|
/**
|
|
207
|
-
* 서버가 알려 준, 다시 시도해도 되는 시각까지의 대기(ms).
|
|
208
|
-
* (`rate_limited`)에만 있다.
|
|
209
|
+
* 서버가 알려 준, 다시 시도해도 되는 시각까지의 대기(ms). 서버가 재시도 시각을 정해 주는 거부
|
|
210
|
+
* (`rate_limited`, 그리고 6.7.0 과 함께 배포된 서버의 `auth_unavailable`)에만 있다.
|
|
209
211
|
*/
|
|
210
212
|
retryAfterMs?: number;
|
|
211
213
|
constructor(init: {
|
|
@@ -232,9 +234,9 @@ declare class RealtimeConnectionError extends Error {
|
|
|
232
234
|
* | `RATE_LIMITED` | 카테고리 또는 연결의 초당 메시지 한도 초과 |
|
|
233
235
|
* | `MESSAGE_TOO_LARGE` | 카테고리 최대 메시지 크기 초과 |
|
|
234
236
|
* | `QUOTA_EXCEEDED` | 플랜 할당량 초과 |
|
|
235
|
-
* | `CATEGORY_NOT_FOUND` | 카테고리가 없거나 비활성 |
|
|
237
|
+
* | `CATEGORY_NOT_FOUND` | 카테고리가 없거나 비활성 (채널 이름 규칙에 어긋난 문자열 채널도 이 코드로 온다) |
|
|
236
238
|
* | `MAX_SUBSCRIBERS_REACHED` | 카테고리 최대 구독자 수 도달 |
|
|
237
|
-
* | `ROOM_ACCESS_DENIED` | room 인가 함수가 거부했다 |
|
|
239
|
+
* | `ROOM_ACCESS_DENIED` | room 인가 함수가 거부했다 (함수가 없거나, 실패하거나, 10초 안에 답하지 않아도 거부로 처리된다) |
|
|
238
240
|
*
|
|
239
241
|
* ```ts
|
|
240
242
|
* try {
|
|
@@ -607,7 +609,12 @@ declare class HttpClient {
|
|
|
607
609
|
* 먼저 보내 Cloudflare 를 두 번 왕복하고, 앱의 "로컬 개발 origin 허용" 이 꺼진 localhost 페이지에서는
|
|
608
610
|
* 키 기반 opt-in 에 막혀 브라우저만 direct 를 못 썼다(Node 는 썼다 — platform-issue 01a15e24).
|
|
609
611
|
* 서버는 이 경로를 키 없이 열어 두고 같은 값을 본문으로도 준다. 본문을 읽으므로 CORS
|
|
610
|
-
* Expose-Headers 에도 기대지 않는다. 실패는
|
|
612
|
+
* Expose-Headers 에도 기대지 않는다. 실패는 호출자에게 던지고 호출자(RealtimeAPI)가 조용히 넘어간다 —
|
|
613
|
+
* direct 는 최적화일 뿐이다.
|
|
614
|
+
*
|
|
615
|
+
* 조회 자체는 같은 baseUrl 의 인스턴스끼리 함께 쓴다(6.7.0+, {@link sharedEdgeHints} 참고). 진행 중인
|
|
616
|
+
* 조회가 있으면 새로 보내지 않고 기다리며, 성공한 결과는 잠시 다시 쓴다. 받아들일지는 이 인스턴스가 정한다 —
|
|
617
|
+
* 이미 배웠거나(`directRealtimeBaseUrl`) direct 를 포기했으면(`directRealtimeDisabled`) 바꾸지 않는다.
|
|
611
618
|
*/
|
|
612
619
|
primeEdgeHints(timeoutMs: number): Promise<void>;
|
|
613
620
|
/**
|
|
@@ -2160,17 +2167,28 @@ declare class AuthAPI {
|
|
|
2160
2167
|
* role: 'editor',
|
|
2161
2168
|
* custom_data: { level: 5 }
|
|
2162
2169
|
* })
|
|
2170
|
+
*
|
|
2171
|
+
* // 역할 여러 개 (6.7.0+): role 은 주 역할(RLS auth.role), roles 는 추가 역할 목록을 **통째로 바꾼다**
|
|
2172
|
+
* // ([] 면 비우고, 빼면 그대로). auth.roles, hasRole(), 실시간 publisher_roles 는 role + roles 를 함께 본다.
|
|
2173
|
+
* await cb.auth.adminUpdateMember('member-uuid', { roles: ['performer'] })
|
|
2174
|
+
* // → { role: 'admin', roles: ['performer'], effective_roles: ['admin', 'performer'], ... }
|
|
2163
2175
|
* ```
|
|
2164
2176
|
*/
|
|
2165
2177
|
adminUpdateMember(memberID: string, fields: {
|
|
2166
2178
|
nickname?: string;
|
|
2167
2179
|
role?: string;
|
|
2180
|
+
/** 추가 역할 목록 — 보내면 통째로 바뀐다(빈 배열이면 비운다). 각 64바이트 이하, role 포함 16개 이하. */
|
|
2181
|
+
roles?: string[];
|
|
2168
2182
|
custom_data?: Record<string, unknown>;
|
|
2169
2183
|
}): Promise<{
|
|
2170
2184
|
member_id: string;
|
|
2171
2185
|
nickname: string;
|
|
2172
2186
|
custom_data: Record<string, unknown>;
|
|
2173
2187
|
role: string;
|
|
2188
|
+
/** 추가 역할 목록 (6.7.0+ 서버) */
|
|
2189
|
+
roles?: string[];
|
|
2190
|
+
/** role + roles (중복 제거, role 이 먼저). RLS auth.roles 와 실시간 publisher_roles 가 보는 값 (6.7.0+ 서버) */
|
|
2191
|
+
effective_roles?: string[];
|
|
2174
2192
|
}>;
|
|
2175
2193
|
/**
|
|
2176
2194
|
* Admin: 다른 멤버를 삭제(탈퇴 처리)합니다. **되돌릴 수 없습니다.**
|
|
@@ -7951,6 +7969,20 @@ declare class QueueAPI {
|
|
|
7951
7969
|
interface SubscribeOptions {
|
|
7952
7970
|
/** 히스토리 로드 개수 (persist=true인 카테고리만) */
|
|
7953
7971
|
historyLimit?: number;
|
|
7972
|
+
/**
|
|
7973
|
+
* 카테고리 안의 room 하나를 구독한다 (6.7.0+, platform-issue 01a15e81). 서버로 가는 채널 이름은
|
|
7974
|
+
* `${category}:${room}` 이다 — `subscribe('concert', { room: 'show-1025' })` 는 `concert:show-1025` 를
|
|
7975
|
+
* 구독한다. 설정(발행 정책, 방송인 역할, persist 등)은 베이스 카테고리(`concert`)의 것을 쓰고, 메시지는
|
|
7976
|
+
* 같은 room 을 구독한 연결끼리만 오간다. 문자열로 `subscribe('concert:show-1025')` 라고 써도 같다.
|
|
7977
|
+
*
|
|
7978
|
+
* - 영문, 숫자, `-`, `_`, `.` 로 1~128자여야 하고, 이때 `category` 는 영문, 숫자, `-`, `_` 만 쓴
|
|
7979
|
+
* 베이스 카테고리 이름이어야 한다(서버의 채널 규칙). 어긋나면 서버에 보내기 전에 에러를 던진다 —
|
|
7980
|
+
* 서버는 잘못된 채널을 `CATEGORY_NOT_FOUND` 로만 알려 원인이 보이지 않는다.
|
|
7981
|
+
* - 카테고리가 `require_room_auth` 이면 서버가 앱의 room 인가 함수를 불러 구독을 허락받는다. 함수가
|
|
7982
|
+
* `{ authorized: true, publisher: true }` 를 돌려주면 이 연결(로그인 멤버)은 그 room 의 방송인이 된다
|
|
7983
|
+
* (`info.isPublisher`, 받는 쪽의 `fromPublisher`).
|
|
7984
|
+
*/
|
|
7985
|
+
room?: string;
|
|
7954
7986
|
}
|
|
7955
7987
|
/**
|
|
7956
7988
|
* 카테고리 발행 정책 (6.6.0+). 콘솔이나 API 로 카테고리에 설정한다.
|
|
@@ -7962,6 +7994,7 @@ interface SubscribeOptions {
|
|
|
7962
7994
|
type RealtimeSendPolicy = "open" | "upstream" | "publishers_only";
|
|
7963
7995
|
/** 카테고리 정보 */
|
|
7964
7996
|
interface CategoryInfo {
|
|
7997
|
+
/** 구독한 채널 이름 (서버가 돌려준 값). room 을 구독했으면 `category:room` 이다. */
|
|
7965
7998
|
category: string;
|
|
7966
7999
|
persist: boolean;
|
|
7967
8000
|
historyCount: number;
|
|
@@ -7969,6 +8002,7 @@ interface CategoryInfo {
|
|
|
7969
8002
|
/**
|
|
7970
8003
|
* 이 연결이 이 카테고리의 방송인인지 (6.6.0+). 카테고리 `publisher_roles` 의 역할을 가진
|
|
7971
8004
|
* **로그인 멤버(accessToken) 연결**만 방송인이다 — 공개 키(익명) 연결은 항상 `false`.
|
|
8005
|
+
* room 채널은 room 인가 함수가 `publisher: true` 를 돌려준 로그인 멤버도 그 room 의 방송인이다 (6.7.0+).
|
|
7972
8006
|
* 구독할 때 정해지므로 역할이나 카테고리 설정이 바뀌면 다시 구독해야 반영된다.
|
|
7973
8007
|
*/
|
|
7974
8008
|
isPublisher: boolean;
|
|
@@ -7978,13 +8012,27 @@ interface CategoryInfo {
|
|
|
7978
8012
|
/** 메시지 데이터 */
|
|
7979
8013
|
interface RealtimeMessage<T = unknown> {
|
|
7980
8014
|
id?: string;
|
|
8015
|
+
/** 메시지가 온 채널 이름. room 채널이면 `category:room` 이다. */
|
|
7981
8016
|
category: string;
|
|
8017
|
+
/**
|
|
8018
|
+
* 보낸 연결의 클라이언트 ID(접속할 때 `client_id` 로 보내는 값). **클라이언트가 정해서 보내는 값이라
|
|
8019
|
+
* 누구나 다른 값을 흉내 낼 수 있다** — 화면 표시나 내 메시지 구분 정도에만 쓰고, 누가 보냈는지
|
|
8020
|
+
* 믿어야 하는 판단(권한, 점수, 신고)에는 {@link RealtimeMessage.fromMember} 를 쓴다.
|
|
8021
|
+
*/
|
|
7982
8022
|
from?: string;
|
|
8023
|
+
/**
|
|
8024
|
+
* 보낸 사람의 앱 멤버 ID — 서버가 검증한 값이다 (6.7.0+, platform-issue 01a15e86). 보낸 연결이
|
|
8025
|
+
* 앱 멤버 `accessToken` 으로 접속했을 때만 있고, 공개 키(익명) 연결이 보낸 메시지에는 없다.
|
|
8026
|
+
* `from` 이나 `data` 안의 값과 달리 클라이언트가 위조할 수 없다. 실시간 메시지와 히스토리 모두에
|
|
8027
|
+
* 실린다(구버전 서버는 보내지 않는다).
|
|
8028
|
+
*/
|
|
8029
|
+
fromMember?: string;
|
|
7983
8030
|
data: T;
|
|
7984
8031
|
sentAt: number;
|
|
7985
8032
|
/**
|
|
7986
|
-
* 보낸 연결이 이
|
|
7987
|
-
* 위조할 수 없다 — 방송인 메시지만 골라 처리할 때 이 값으로 가린다.
|
|
8033
|
+
* 보낸 연결이 이 카테고리(room 채널이면 그 room)의 방송인이라는 **서버 보증** (6.6.0+). `data` 안의
|
|
8034
|
+
* 값과 달리 클라이언트가 위조할 수 없다 — 방송인 메시지만 골라 처리할 때 이 값으로 가린다.
|
|
8035
|
+
* 히스토리 메시지에는 없다.
|
|
7988
8036
|
*/
|
|
7989
8037
|
fromPublisher?: boolean;
|
|
7990
8038
|
}
|
|
@@ -8045,6 +8093,17 @@ interface SendOptions {
|
|
|
8045
8093
|
interface Subscription {
|
|
8046
8094
|
/** 카테고리 정보 */
|
|
8047
8095
|
info: CategoryInfo;
|
|
8096
|
+
/**
|
|
8097
|
+
* 서버에 구독한 채널 이름 (6.7.0+). room 을 구독했으면 `category:room`, 아니면 카테고리 이름이다.
|
|
8098
|
+
* `realtime.unsubscribe()`, `realtime.markRead()`, `realtime.onReadReceipt()` 처럼 이름을 받는
|
|
8099
|
+
* 메서드에는 이 값을 넘긴다.
|
|
8100
|
+
*/
|
|
8101
|
+
readonly channel: string;
|
|
8102
|
+
/**
|
|
8103
|
+
* 구독한 room (6.7.0+). `subscribe(category, { room })` 이나 `subscribe('category:room')` 으로
|
|
8104
|
+
* 구독했을 때만 있다.
|
|
8105
|
+
*/
|
|
8106
|
+
readonly room?: string;
|
|
8048
8107
|
/** 메시지 전송 */
|
|
8049
8108
|
send<T = unknown>(data: T, options?: SendOptions): Promise<void>;
|
|
8050
8109
|
/** 히스토리 조회 (persist=true인 경우만) */
|
|
@@ -8299,21 +8358,35 @@ declare class RealtimeAPI {
|
|
|
8299
8358
|
disconnect(): void;
|
|
8300
8359
|
/**
|
|
8301
8360
|
* 카테고리 구독
|
|
8361
|
+
*
|
|
8362
|
+
* `options.room` 을 주면 그 카테고리의 room 하나를 구독한다 (6.7.0+). 서버 채널 이름은
|
|
8363
|
+
* `${category}:${room}` 이고, 구독 객체의 `send` / `getHistory` / `unsubscribe` / `onMessage`, 재연결 뒤
|
|
8364
|
+
* 다시 구독하기는 모두 이 채널 이름으로 동작한다(`subscription.channel`).
|
|
8365
|
+
*
|
|
8366
|
+
* @example
|
|
8367
|
+
* ```typescript
|
|
8368
|
+
* const stage = await client.realtime.subscribe('concert', { room: 'show-1025' })
|
|
8369
|
+
* stage.channel // 'concert:show-1025'
|
|
8370
|
+
* stage.room // 'show-1025'
|
|
8371
|
+
* stage.info.isPublisher // room 인가 함수가 publisher: true 를 돌려준 로그인 멤버면 true
|
|
8372
|
+
* ```
|
|
8302
8373
|
*/
|
|
8303
8374
|
subscribe(category: string, options?: SubscribeOptions): Promise<Subscription>;
|
|
8304
8375
|
/**
|
|
8305
8376
|
* 구독 해제
|
|
8377
|
+
* @param category 카테고리 이름. room 을 구독했으면 채널 이름(`category:room`, `subscription.channel`)
|
|
8306
8378
|
*/
|
|
8307
8379
|
unsubscribe(category: string): Promise<void>;
|
|
8308
8380
|
/**
|
|
8309
8381
|
* 메시지 전송
|
|
8310
|
-
* @param category 카테고리 이름
|
|
8382
|
+
* @param category 카테고리 이름. room 을 구독했으면 채널 이름(`category:room`, `subscription.channel`)
|
|
8311
8383
|
* @param data 전송할 데이터
|
|
8312
8384
|
* @param options 전송 옵션 (includeSelf: 발신자도 메시지 수신 여부, 기본값 true)
|
|
8313
8385
|
*/
|
|
8314
8386
|
sendMessage<T = unknown>(category: string, data: T, options?: SendOptions): Promise<void>;
|
|
8315
8387
|
/**
|
|
8316
8388
|
* 히스토리 조회
|
|
8389
|
+
* @param category 카테고리 이름. room 을 구독했으면 채널 이름(`category:room`, `subscription.channel`)
|
|
8317
8390
|
*/
|
|
8318
8391
|
getHistory<T = unknown>(category: string, limit?: number): Promise<HistoryResponse<T>>;
|
|
8319
8392
|
/**
|
|
@@ -8562,18 +8635,25 @@ declare class RealtimeAPI {
|
|
|
8562
8635
|
* 한국 사용자는 미국(LAX)을 거친 ~270ms 로 붙었다(platform-issue 01a15d9d 의 273ms). 응답 헤더는
|
|
8563
8636
|
* HttpClient 가 평소처럼 학습한다. 광고 대상이 아니면(해외, 킬 스위치) 빈 응답이라 원래 소켓
|
|
8564
8637
|
* URL 로 간다. 실패하거나 느리면(1.5초) 그냥 넘어간다 — 최적화일 뿐 연결의 전제가 아니다.
|
|
8565
|
-
*
|
|
8638
|
+
* 인스턴스당 한 번만 묻고, 같은 baseUrl 의 인스턴스끼리는 진행 중인 조회와 배운 결과를 함께 쓴다
|
|
8639
|
+
* (6.7.0+, HttpClient.primeEdgeHints 참고 — 한 페이지의 인스턴스 10개가 동시에 물어 대부분 1.5초를 넘기던
|
|
8640
|
+
* platform-issue 01a15e81).
|
|
8566
8641
|
*/
|
|
8567
8642
|
private primeDirectRealtime;
|
|
8568
8643
|
/**
|
|
8569
8644
|
* 첫 연결을 연결 제한 시간(`timeout`) 안에서 다시 시도한다 (6.6.0+, platform-issue 01a15e24).
|
|
8570
8645
|
*
|
|
8571
|
-
* - 서버가 재시도 시각을 알려 준 거부(`retryAfterMs`, 예: 한 IP 에서 연결이 몰린 `rate_limited
|
|
8572
|
-
* 그 시각에 지터를 더해 다시 붙는다. 공연 시작처럼 한꺼번에 들어올 때
|
|
8646
|
+
* - 서버가 재시도 시각을 알려 준 거부(`retryAfterMs`, 예: 한 IP 에서 연결이 몰린 `rate_limited`, 인증 서버가
|
|
8647
|
+
* 잠깐 닿지 않는 `auth_unavailable`): 그 시각에 지터를 더해 다시 붙는다. 공연 시작처럼 한꺼번에 들어올 때
|
|
8648
|
+
* 앱이 재시도를 짜지 않아도 된다.
|
|
8649
|
+
* - `rate_limited` / `auth_unavailable` 인데 재시도 시각이 없으면(사유 프레임 없이 close 코드만 왔거나 구버전
|
|
8650
|
+
* 서버) 지수 백오프로 다시 한다. `auth_unavailable` 은 6.6.0 까지 바로 던져, 인증 서버가 몇 초 흔들리는
|
|
8651
|
+
* 동안 들어온 관객이 전부 연결에 실패했다(6.7.0, platform-issue 01a15e81).
|
|
8573
8652
|
* - 이유 없는 핸드셰이크 실패: 두 번까지 백오프 후 다시 한다. 네트워크가 WebSocket 을 실제로 막으면
|
|
8574
8653
|
* 매번 실패하고, 앞단의 일시 거부는 다시 하면 붙는다 — 한 번 실패로 SSE 폴백하던 것이 연결 속도
|
|
8575
8654
|
* 제한을 "네트워크 차단" 으로 오진한 원인이었다.
|
|
8576
|
-
* - 그 밖의 거부(자격증명, 동시접속 한도 등)와 시간 초과는 바로 던진다.
|
|
8655
|
+
* - 그 밖의 거부(자격증명, 동시접속 한도 등)와 시간 초과는 바로 던진다. 다시 시도할 시간이 남지 않아도
|
|
8656
|
+
* 마지막 거부를 그대로 던진다(`retryAfterMs` 가 있으면 앱이 그 시각에 다시 부를 수 있다).
|
|
8577
8657
|
*/
|
|
8578
8658
|
private connectWithRetry;
|
|
8579
8659
|
private doConnect;
|
package/dist/index.d.ts
CHANGED
|
@@ -161,7 +161,7 @@ declare class GameError extends Error {
|
|
|
161
161
|
* | `quota_exceeded` | 월간 WebSocket 사용 시간 소진 | 불가 | 플랜 업그레이드 전까지 재시도 무의미 |
|
|
162
162
|
* | `server_at_capacity` | 서버 일시 혼잡 | 가능 | 잠시 후 재시도 |
|
|
163
163
|
* | `rate_limited` | 한 IP 에서 연결 시도가 너무 잦음 (6.6.0+) | 가능 | `connect()` 가 `retryAfterMs` 만큼(지터 포함) 기다렸다 스스로 다시 붙는다. 연결 제한 시간 안에 못 붙으면 이 에러를 던진다 |
|
|
164
|
-
* | `auth_unavailable` | 인증 서버 일시 장애 | 가능 |
|
|
164
|
+
* | `auth_unavailable` | 인증 서버 일시 장애 | 가능 | `connect()` 가 `retryAfterMs` 만큼(없으면 지수 백오프) 기다렸다 스스로 다시 붙는다 (6.7.0+). 연결 제한 시간 안에 못 붙으면 이 에러를 던진다 |
|
|
165
165
|
* | `unauthorized` | 공개 키/액세스 토큰이 유효하지 않음 | 불가 | 키·토큰 확인 (토큰 만료면 새 토큰으로 `connect()`) |
|
|
166
166
|
* | `forbidden` | 다른 앱의 경로로 접속 | 불가 | 설정 확인 |
|
|
167
167
|
* | `invalid_user_id` | 공개 키 연결에 UUID 형식 `userId` | 불가 | `accessToken` 을 쓰거나 UUID 가 아닌 식별자 사용 |
|
|
@@ -177,8 +177,10 @@ type RealtimeConnectionErrorCode = "connection_limit_exceeded" | "quota_exceeded
|
|
|
177
177
|
* 연결을 받은 뒤 거부 이유를 보내고 4000+HTTP상태 close 코드(예: 4401, 4402, 4503)로 닫는다.
|
|
178
178
|
*
|
|
179
179
|
* - 거부는 SSE 폴백 대상이 아니다 — 네트워크는 정상이고 서버가 받지 않은 것이다.
|
|
180
|
-
* - 서버가 재시도 시각을 알려 준 거부(`retryAfterMs`, 예: `rate_limited`)는 `connect()` 가
|
|
181
|
-
* 시간(`timeout`) 안에서 그 시각에 지터를 더해 스스로 다시 시도한다.
|
|
180
|
+
* - 서버가 재시도 시각을 알려 준 거부(`retryAfterMs`, 예: `rate_limited`, `auth_unavailable`)는 `connect()` 가
|
|
181
|
+
* 연결 제한 시간(`timeout`) 안에서 그 시각에 지터를 더해 스스로 다시 시도한다. `rate_limited` 와
|
|
182
|
+
* `auth_unavailable` 은 시각이 없어도(구버전 서버) 지수 백오프로 다시 시도한다(6.7.0+). 그 밖의 거부
|
|
183
|
+
* (`unauthorized`, `connection_limit_exceeded` 등)는 바로 던진다.
|
|
182
184
|
* - 자동 재연결 중에 받은 거부는 `retryable` 이면 백오프로 계속 시도하고, 아니면 재연결을 멈추고
|
|
183
185
|
* `cb.realtime.onError` 로 이 에러를 알린다.
|
|
184
186
|
*
|
|
@@ -204,8 +206,8 @@ declare class RealtimeConnectionError extends Error {
|
|
|
204
206
|
/** 거부 상세 (예: connection_limit_exceeded 의 `current`/`limit`, quota_exceeded 의 `quota`/`unit`). */
|
|
205
207
|
details?: Record<string, unknown>;
|
|
206
208
|
/**
|
|
207
|
-
* 서버가 알려 준, 다시 시도해도 되는 시각까지의 대기(ms).
|
|
208
|
-
* (`rate_limited`)에만 있다.
|
|
209
|
+
* 서버가 알려 준, 다시 시도해도 되는 시각까지의 대기(ms). 서버가 재시도 시각을 정해 주는 거부
|
|
210
|
+
* (`rate_limited`, 그리고 6.7.0 과 함께 배포된 서버의 `auth_unavailable`)에만 있다.
|
|
209
211
|
*/
|
|
210
212
|
retryAfterMs?: number;
|
|
211
213
|
constructor(init: {
|
|
@@ -232,9 +234,9 @@ declare class RealtimeConnectionError extends Error {
|
|
|
232
234
|
* | `RATE_LIMITED` | 카테고리 또는 연결의 초당 메시지 한도 초과 |
|
|
233
235
|
* | `MESSAGE_TOO_LARGE` | 카테고리 최대 메시지 크기 초과 |
|
|
234
236
|
* | `QUOTA_EXCEEDED` | 플랜 할당량 초과 |
|
|
235
|
-
* | `CATEGORY_NOT_FOUND` | 카테고리가 없거나 비활성 |
|
|
237
|
+
* | `CATEGORY_NOT_FOUND` | 카테고리가 없거나 비활성 (채널 이름 규칙에 어긋난 문자열 채널도 이 코드로 온다) |
|
|
236
238
|
* | `MAX_SUBSCRIBERS_REACHED` | 카테고리 최대 구독자 수 도달 |
|
|
237
|
-
* | `ROOM_ACCESS_DENIED` | room 인가 함수가 거부했다 |
|
|
239
|
+
* | `ROOM_ACCESS_DENIED` | room 인가 함수가 거부했다 (함수가 없거나, 실패하거나, 10초 안에 답하지 않아도 거부로 처리된다) |
|
|
238
240
|
*
|
|
239
241
|
* ```ts
|
|
240
242
|
* try {
|
|
@@ -607,7 +609,12 @@ declare class HttpClient {
|
|
|
607
609
|
* 먼저 보내 Cloudflare 를 두 번 왕복하고, 앱의 "로컬 개발 origin 허용" 이 꺼진 localhost 페이지에서는
|
|
608
610
|
* 키 기반 opt-in 에 막혀 브라우저만 direct 를 못 썼다(Node 는 썼다 — platform-issue 01a15e24).
|
|
609
611
|
* 서버는 이 경로를 키 없이 열어 두고 같은 값을 본문으로도 준다. 본문을 읽으므로 CORS
|
|
610
|
-
* Expose-Headers 에도 기대지 않는다. 실패는
|
|
612
|
+
* Expose-Headers 에도 기대지 않는다. 실패는 호출자에게 던지고 호출자(RealtimeAPI)가 조용히 넘어간다 —
|
|
613
|
+
* direct 는 최적화일 뿐이다.
|
|
614
|
+
*
|
|
615
|
+
* 조회 자체는 같은 baseUrl 의 인스턴스끼리 함께 쓴다(6.7.0+, {@link sharedEdgeHints} 참고). 진행 중인
|
|
616
|
+
* 조회가 있으면 새로 보내지 않고 기다리며, 성공한 결과는 잠시 다시 쓴다. 받아들일지는 이 인스턴스가 정한다 —
|
|
617
|
+
* 이미 배웠거나(`directRealtimeBaseUrl`) direct 를 포기했으면(`directRealtimeDisabled`) 바꾸지 않는다.
|
|
611
618
|
*/
|
|
612
619
|
primeEdgeHints(timeoutMs: number): Promise<void>;
|
|
613
620
|
/**
|
|
@@ -2160,17 +2167,28 @@ declare class AuthAPI {
|
|
|
2160
2167
|
* role: 'editor',
|
|
2161
2168
|
* custom_data: { level: 5 }
|
|
2162
2169
|
* })
|
|
2170
|
+
*
|
|
2171
|
+
* // 역할 여러 개 (6.7.0+): role 은 주 역할(RLS auth.role), roles 는 추가 역할 목록을 **통째로 바꾼다**
|
|
2172
|
+
* // ([] 면 비우고, 빼면 그대로). auth.roles, hasRole(), 실시간 publisher_roles 는 role + roles 를 함께 본다.
|
|
2173
|
+
* await cb.auth.adminUpdateMember('member-uuid', { roles: ['performer'] })
|
|
2174
|
+
* // → { role: 'admin', roles: ['performer'], effective_roles: ['admin', 'performer'], ... }
|
|
2163
2175
|
* ```
|
|
2164
2176
|
*/
|
|
2165
2177
|
adminUpdateMember(memberID: string, fields: {
|
|
2166
2178
|
nickname?: string;
|
|
2167
2179
|
role?: string;
|
|
2180
|
+
/** 추가 역할 목록 — 보내면 통째로 바뀐다(빈 배열이면 비운다). 각 64바이트 이하, role 포함 16개 이하. */
|
|
2181
|
+
roles?: string[];
|
|
2168
2182
|
custom_data?: Record<string, unknown>;
|
|
2169
2183
|
}): Promise<{
|
|
2170
2184
|
member_id: string;
|
|
2171
2185
|
nickname: string;
|
|
2172
2186
|
custom_data: Record<string, unknown>;
|
|
2173
2187
|
role: string;
|
|
2188
|
+
/** 추가 역할 목록 (6.7.0+ 서버) */
|
|
2189
|
+
roles?: string[];
|
|
2190
|
+
/** role + roles (중복 제거, role 이 먼저). RLS auth.roles 와 실시간 publisher_roles 가 보는 값 (6.7.0+ 서버) */
|
|
2191
|
+
effective_roles?: string[];
|
|
2174
2192
|
}>;
|
|
2175
2193
|
/**
|
|
2176
2194
|
* Admin: 다른 멤버를 삭제(탈퇴 처리)합니다. **되돌릴 수 없습니다.**
|
|
@@ -7951,6 +7969,20 @@ declare class QueueAPI {
|
|
|
7951
7969
|
interface SubscribeOptions {
|
|
7952
7970
|
/** 히스토리 로드 개수 (persist=true인 카테고리만) */
|
|
7953
7971
|
historyLimit?: number;
|
|
7972
|
+
/**
|
|
7973
|
+
* 카테고리 안의 room 하나를 구독한다 (6.7.0+, platform-issue 01a15e81). 서버로 가는 채널 이름은
|
|
7974
|
+
* `${category}:${room}` 이다 — `subscribe('concert', { room: 'show-1025' })` 는 `concert:show-1025` 를
|
|
7975
|
+
* 구독한다. 설정(발행 정책, 방송인 역할, persist 등)은 베이스 카테고리(`concert`)의 것을 쓰고, 메시지는
|
|
7976
|
+
* 같은 room 을 구독한 연결끼리만 오간다. 문자열로 `subscribe('concert:show-1025')` 라고 써도 같다.
|
|
7977
|
+
*
|
|
7978
|
+
* - 영문, 숫자, `-`, `_`, `.` 로 1~128자여야 하고, 이때 `category` 는 영문, 숫자, `-`, `_` 만 쓴
|
|
7979
|
+
* 베이스 카테고리 이름이어야 한다(서버의 채널 규칙). 어긋나면 서버에 보내기 전에 에러를 던진다 —
|
|
7980
|
+
* 서버는 잘못된 채널을 `CATEGORY_NOT_FOUND` 로만 알려 원인이 보이지 않는다.
|
|
7981
|
+
* - 카테고리가 `require_room_auth` 이면 서버가 앱의 room 인가 함수를 불러 구독을 허락받는다. 함수가
|
|
7982
|
+
* `{ authorized: true, publisher: true }` 를 돌려주면 이 연결(로그인 멤버)은 그 room 의 방송인이 된다
|
|
7983
|
+
* (`info.isPublisher`, 받는 쪽의 `fromPublisher`).
|
|
7984
|
+
*/
|
|
7985
|
+
room?: string;
|
|
7954
7986
|
}
|
|
7955
7987
|
/**
|
|
7956
7988
|
* 카테고리 발행 정책 (6.6.0+). 콘솔이나 API 로 카테고리에 설정한다.
|
|
@@ -7962,6 +7994,7 @@ interface SubscribeOptions {
|
|
|
7962
7994
|
type RealtimeSendPolicy = "open" | "upstream" | "publishers_only";
|
|
7963
7995
|
/** 카테고리 정보 */
|
|
7964
7996
|
interface CategoryInfo {
|
|
7997
|
+
/** 구독한 채널 이름 (서버가 돌려준 값). room 을 구독했으면 `category:room` 이다. */
|
|
7965
7998
|
category: string;
|
|
7966
7999
|
persist: boolean;
|
|
7967
8000
|
historyCount: number;
|
|
@@ -7969,6 +8002,7 @@ interface CategoryInfo {
|
|
|
7969
8002
|
/**
|
|
7970
8003
|
* 이 연결이 이 카테고리의 방송인인지 (6.6.0+). 카테고리 `publisher_roles` 의 역할을 가진
|
|
7971
8004
|
* **로그인 멤버(accessToken) 연결**만 방송인이다 — 공개 키(익명) 연결은 항상 `false`.
|
|
8005
|
+
* room 채널은 room 인가 함수가 `publisher: true` 를 돌려준 로그인 멤버도 그 room 의 방송인이다 (6.7.0+).
|
|
7972
8006
|
* 구독할 때 정해지므로 역할이나 카테고리 설정이 바뀌면 다시 구독해야 반영된다.
|
|
7973
8007
|
*/
|
|
7974
8008
|
isPublisher: boolean;
|
|
@@ -7978,13 +8012,27 @@ interface CategoryInfo {
|
|
|
7978
8012
|
/** 메시지 데이터 */
|
|
7979
8013
|
interface RealtimeMessage<T = unknown> {
|
|
7980
8014
|
id?: string;
|
|
8015
|
+
/** 메시지가 온 채널 이름. room 채널이면 `category:room` 이다. */
|
|
7981
8016
|
category: string;
|
|
8017
|
+
/**
|
|
8018
|
+
* 보낸 연결의 클라이언트 ID(접속할 때 `client_id` 로 보내는 값). **클라이언트가 정해서 보내는 값이라
|
|
8019
|
+
* 누구나 다른 값을 흉내 낼 수 있다** — 화면 표시나 내 메시지 구분 정도에만 쓰고, 누가 보냈는지
|
|
8020
|
+
* 믿어야 하는 판단(권한, 점수, 신고)에는 {@link RealtimeMessage.fromMember} 를 쓴다.
|
|
8021
|
+
*/
|
|
7982
8022
|
from?: string;
|
|
8023
|
+
/**
|
|
8024
|
+
* 보낸 사람의 앱 멤버 ID — 서버가 검증한 값이다 (6.7.0+, platform-issue 01a15e86). 보낸 연결이
|
|
8025
|
+
* 앱 멤버 `accessToken` 으로 접속했을 때만 있고, 공개 키(익명) 연결이 보낸 메시지에는 없다.
|
|
8026
|
+
* `from` 이나 `data` 안의 값과 달리 클라이언트가 위조할 수 없다. 실시간 메시지와 히스토리 모두에
|
|
8027
|
+
* 실린다(구버전 서버는 보내지 않는다).
|
|
8028
|
+
*/
|
|
8029
|
+
fromMember?: string;
|
|
7983
8030
|
data: T;
|
|
7984
8031
|
sentAt: number;
|
|
7985
8032
|
/**
|
|
7986
|
-
* 보낸 연결이 이
|
|
7987
|
-
* 위조할 수 없다 — 방송인 메시지만 골라 처리할 때 이 값으로 가린다.
|
|
8033
|
+
* 보낸 연결이 이 카테고리(room 채널이면 그 room)의 방송인이라는 **서버 보증** (6.6.0+). `data` 안의
|
|
8034
|
+
* 값과 달리 클라이언트가 위조할 수 없다 — 방송인 메시지만 골라 처리할 때 이 값으로 가린다.
|
|
8035
|
+
* 히스토리 메시지에는 없다.
|
|
7988
8036
|
*/
|
|
7989
8037
|
fromPublisher?: boolean;
|
|
7990
8038
|
}
|
|
@@ -8045,6 +8093,17 @@ interface SendOptions {
|
|
|
8045
8093
|
interface Subscription {
|
|
8046
8094
|
/** 카테고리 정보 */
|
|
8047
8095
|
info: CategoryInfo;
|
|
8096
|
+
/**
|
|
8097
|
+
* 서버에 구독한 채널 이름 (6.7.0+). room 을 구독했으면 `category:room`, 아니면 카테고리 이름이다.
|
|
8098
|
+
* `realtime.unsubscribe()`, `realtime.markRead()`, `realtime.onReadReceipt()` 처럼 이름을 받는
|
|
8099
|
+
* 메서드에는 이 값을 넘긴다.
|
|
8100
|
+
*/
|
|
8101
|
+
readonly channel: string;
|
|
8102
|
+
/**
|
|
8103
|
+
* 구독한 room (6.7.0+). `subscribe(category, { room })` 이나 `subscribe('category:room')` 으로
|
|
8104
|
+
* 구독했을 때만 있다.
|
|
8105
|
+
*/
|
|
8106
|
+
readonly room?: string;
|
|
8048
8107
|
/** 메시지 전송 */
|
|
8049
8108
|
send<T = unknown>(data: T, options?: SendOptions): Promise<void>;
|
|
8050
8109
|
/** 히스토리 조회 (persist=true인 경우만) */
|
|
@@ -8299,21 +8358,35 @@ declare class RealtimeAPI {
|
|
|
8299
8358
|
disconnect(): void;
|
|
8300
8359
|
/**
|
|
8301
8360
|
* 카테고리 구독
|
|
8361
|
+
*
|
|
8362
|
+
* `options.room` 을 주면 그 카테고리의 room 하나를 구독한다 (6.7.0+). 서버 채널 이름은
|
|
8363
|
+
* `${category}:${room}` 이고, 구독 객체의 `send` / `getHistory` / `unsubscribe` / `onMessage`, 재연결 뒤
|
|
8364
|
+
* 다시 구독하기는 모두 이 채널 이름으로 동작한다(`subscription.channel`).
|
|
8365
|
+
*
|
|
8366
|
+
* @example
|
|
8367
|
+
* ```typescript
|
|
8368
|
+
* const stage = await client.realtime.subscribe('concert', { room: 'show-1025' })
|
|
8369
|
+
* stage.channel // 'concert:show-1025'
|
|
8370
|
+
* stage.room // 'show-1025'
|
|
8371
|
+
* stage.info.isPublisher // room 인가 함수가 publisher: true 를 돌려준 로그인 멤버면 true
|
|
8372
|
+
* ```
|
|
8302
8373
|
*/
|
|
8303
8374
|
subscribe(category: string, options?: SubscribeOptions): Promise<Subscription>;
|
|
8304
8375
|
/**
|
|
8305
8376
|
* 구독 해제
|
|
8377
|
+
* @param category 카테고리 이름. room 을 구독했으면 채널 이름(`category:room`, `subscription.channel`)
|
|
8306
8378
|
*/
|
|
8307
8379
|
unsubscribe(category: string): Promise<void>;
|
|
8308
8380
|
/**
|
|
8309
8381
|
* 메시지 전송
|
|
8310
|
-
* @param category 카테고리 이름
|
|
8382
|
+
* @param category 카테고리 이름. room 을 구독했으면 채널 이름(`category:room`, `subscription.channel`)
|
|
8311
8383
|
* @param data 전송할 데이터
|
|
8312
8384
|
* @param options 전송 옵션 (includeSelf: 발신자도 메시지 수신 여부, 기본값 true)
|
|
8313
8385
|
*/
|
|
8314
8386
|
sendMessage<T = unknown>(category: string, data: T, options?: SendOptions): Promise<void>;
|
|
8315
8387
|
/**
|
|
8316
8388
|
* 히스토리 조회
|
|
8389
|
+
* @param category 카테고리 이름. room 을 구독했으면 채널 이름(`category:room`, `subscription.channel`)
|
|
8317
8390
|
*/
|
|
8318
8391
|
getHistory<T = unknown>(category: string, limit?: number): Promise<HistoryResponse<T>>;
|
|
8319
8392
|
/**
|
|
@@ -8562,18 +8635,25 @@ declare class RealtimeAPI {
|
|
|
8562
8635
|
* 한국 사용자는 미국(LAX)을 거친 ~270ms 로 붙었다(platform-issue 01a15d9d 의 273ms). 응답 헤더는
|
|
8563
8636
|
* HttpClient 가 평소처럼 학습한다. 광고 대상이 아니면(해외, 킬 스위치) 빈 응답이라 원래 소켓
|
|
8564
8637
|
* URL 로 간다. 실패하거나 느리면(1.5초) 그냥 넘어간다 — 최적화일 뿐 연결의 전제가 아니다.
|
|
8565
|
-
*
|
|
8638
|
+
* 인스턴스당 한 번만 묻고, 같은 baseUrl 의 인스턴스끼리는 진행 중인 조회와 배운 결과를 함께 쓴다
|
|
8639
|
+
* (6.7.0+, HttpClient.primeEdgeHints 참고 — 한 페이지의 인스턴스 10개가 동시에 물어 대부분 1.5초를 넘기던
|
|
8640
|
+
* platform-issue 01a15e81).
|
|
8566
8641
|
*/
|
|
8567
8642
|
private primeDirectRealtime;
|
|
8568
8643
|
/**
|
|
8569
8644
|
* 첫 연결을 연결 제한 시간(`timeout`) 안에서 다시 시도한다 (6.6.0+, platform-issue 01a15e24).
|
|
8570
8645
|
*
|
|
8571
|
-
* - 서버가 재시도 시각을 알려 준 거부(`retryAfterMs`, 예: 한 IP 에서 연결이 몰린 `rate_limited
|
|
8572
|
-
* 그 시각에 지터를 더해 다시 붙는다. 공연 시작처럼 한꺼번에 들어올 때
|
|
8646
|
+
* - 서버가 재시도 시각을 알려 준 거부(`retryAfterMs`, 예: 한 IP 에서 연결이 몰린 `rate_limited`, 인증 서버가
|
|
8647
|
+
* 잠깐 닿지 않는 `auth_unavailable`): 그 시각에 지터를 더해 다시 붙는다. 공연 시작처럼 한꺼번에 들어올 때
|
|
8648
|
+
* 앱이 재시도를 짜지 않아도 된다.
|
|
8649
|
+
* - `rate_limited` / `auth_unavailable` 인데 재시도 시각이 없으면(사유 프레임 없이 close 코드만 왔거나 구버전
|
|
8650
|
+
* 서버) 지수 백오프로 다시 한다. `auth_unavailable` 은 6.6.0 까지 바로 던져, 인증 서버가 몇 초 흔들리는
|
|
8651
|
+
* 동안 들어온 관객이 전부 연결에 실패했다(6.7.0, platform-issue 01a15e81).
|
|
8573
8652
|
* - 이유 없는 핸드셰이크 실패: 두 번까지 백오프 후 다시 한다. 네트워크가 WebSocket 을 실제로 막으면
|
|
8574
8653
|
* 매번 실패하고, 앞단의 일시 거부는 다시 하면 붙는다 — 한 번 실패로 SSE 폴백하던 것이 연결 속도
|
|
8575
8654
|
* 제한을 "네트워크 차단" 으로 오진한 원인이었다.
|
|
8576
|
-
* - 그 밖의 거부(자격증명, 동시접속 한도 등)와 시간 초과는 바로 던진다.
|
|
8655
|
+
* - 그 밖의 거부(자격증명, 동시접속 한도 등)와 시간 초과는 바로 던진다. 다시 시도할 시간이 남지 않아도
|
|
8656
|
+
* 마지막 거부를 그대로 던진다(`retryAfterMs` 가 있으면 앱이 그 시각에 다시 부를 수 있다).
|
|
8577
8657
|
*/
|
|
8578
8658
|
private connectWithRetry;
|
|
8579
8659
|
private doConnect;
|