connectbase-client 5.13.0 → 6.0.1
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 +91 -0
- package/README.md +38 -0
- package/dist/connect-base.umd.js +5 -5
- package/dist/index.d.mts +92 -1
- package/dist/index.d.ts +92 -1
- package/dist/index.js +179 -12
- package/dist/index.mjs +179 -12
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -329,6 +329,11 @@ declare class HttpClient {
|
|
|
329
329
|
private markSessionHint;
|
|
330
330
|
private clearSessionHint;
|
|
331
331
|
private hasSessionHint;
|
|
332
|
+
private buildNoCookieSessionKey;
|
|
333
|
+
private markNoCookieSession;
|
|
334
|
+
private clearNoCookieSessionMark;
|
|
335
|
+
/** 쿠키 전용 refresh 를 지금 건너뛰어야 하는지 (최근에 401/403 을 받았는지). */
|
|
336
|
+
private isNoCookieSessionMarkFresh;
|
|
332
337
|
/**
|
|
333
338
|
* OAuth redirect callback 직후 호출되어 HttpOnly cookie 를 부트스트랩한다.
|
|
334
339
|
*
|
|
@@ -7403,9 +7408,31 @@ declare class RealtimeAPI {
|
|
|
7403
7408
|
*/
|
|
7404
7409
|
getState(): ConnectionState;
|
|
7405
7410
|
/**
|
|
7406
|
-
*
|
|
7411
|
+
* pub/sub 사용 가능 여부.
|
|
7412
|
+
*
|
|
7413
|
+
* WebSocket 으로 연결됐을 때만 true 다. SSE fallback 모드에서는 `subscribe` /
|
|
7414
|
+
* `sendMessage` / `setPresence` / typing 이 전부 예외를 던지므로, 여기서 true 를
|
|
7415
|
+
* 반환하면 앱이 "연결됨"으로 오판하고 아무것도 오가지 않는 채 조용히 멈춘다
|
|
7416
|
+
* (platform-issue 01a01e6a-3cb1).
|
|
7417
|
+
*
|
|
7418
|
+
* AI 스트리밍만 필요하다면 {@link isStreamReady} 를, 다운그레이드 여부를 알고 싶다면
|
|
7419
|
+
* {@link isDegraded} 를 쓴다.
|
|
7407
7420
|
*/
|
|
7408
7421
|
isConnected(): boolean;
|
|
7422
|
+
/**
|
|
7423
|
+
* SSE fallback 으로 다운그레이드된 상태인지 여부.
|
|
7424
|
+
*
|
|
7425
|
+
* true 면 연결은 살아 있지만 단방향이라 `realtime.stream()` 만 동작한다. 사용자에게
|
|
7426
|
+
* "제한된 네트워크라 일부 기능을 쓸 수 없다" 고 안내할 지점이다.
|
|
7427
|
+
*/
|
|
7428
|
+
isDegraded(): boolean;
|
|
7429
|
+
/**
|
|
7430
|
+
* AI 스트리밍(`realtime.stream()`) 사용 가능 여부.
|
|
7431
|
+
*
|
|
7432
|
+
* WS 든 SSE fallback 이든 연결만 서 있으면 true 다. 종전 `isConnected()` 와 같은
|
|
7433
|
+
* 판정이므로, 스트리밍 게이팅에 `isConnected()` 를 쓰던 코드는 이쪽으로 옮긴다.
|
|
7434
|
+
*/
|
|
7435
|
+
isStreamReady(): boolean;
|
|
7409
7436
|
/**
|
|
7410
7437
|
* 상태 변경 핸들러 등록
|
|
7411
7438
|
*/
|
|
@@ -10047,6 +10074,14 @@ interface ICEServer {
|
|
|
10047
10074
|
/** ICE 서버 목록 응답 */
|
|
10048
10075
|
interface ICEServersResponse {
|
|
10049
10076
|
ice_servers: ICEServer[];
|
|
10077
|
+
/**
|
|
10078
|
+
* ephemeral TURN 자격증명의 만료 시각(Unix epoch 초).
|
|
10079
|
+
*
|
|
10080
|
+
* 서버가 `TURN_SHARED_SECRET` 으로 시간제한 자격증명을 발급한 경우에만 0 이 아니다.
|
|
10081
|
+
* STUN 전용 구성에서는 0 — 만료가 없다는 뜻이다. 통화가 길어질 수 있으면 이 시각
|
|
10082
|
+
* 전에 `getICEServers()` 를 다시 불러 새 자격증명을 받는다.
|
|
10083
|
+
*/
|
|
10084
|
+
expires_at?: number;
|
|
10050
10085
|
}
|
|
10051
10086
|
/**
|
|
10052
10087
|
* 채널별 통계 (룸 ID 의 `channel:room` 컨벤션 기준 집계).
|
|
@@ -10122,6 +10157,16 @@ declare class WebRTCAPI {
|
|
|
10122
10157
|
private reconnectAttempts;
|
|
10123
10158
|
private maxReconnectAttempts;
|
|
10124
10159
|
private reconnectTimeout;
|
|
10160
|
+
/**
|
|
10161
|
+
* 시그널링 소켓의 세대(generation) 토큰.
|
|
10162
|
+
*
|
|
10163
|
+
* 소켓을 열 때마다 1 증가하며, 각 핸들러는 자기가 열릴 때의 세대를 클로저에 담아둔다.
|
|
10164
|
+
* 세대가 밀린 핸들러는 "옛 소켓의 지연 이벤트" 이므로 인스턴스 상태를 건드리지 않는다.
|
|
10165
|
+
* 참조(`this.ws`)만 끊는 것으로는 부족하다 — 핸들러 클로저가 인스턴스를 캡처하고 있어
|
|
10166
|
+
* close 이벤트가 뒤늦게 도착하면 새 연결을 'disconnected' 로 덮어쓰고 새로 만든 피어
|
|
10167
|
+
* 연결까지 정리해버린다.
|
|
10168
|
+
*/
|
|
10169
|
+
private wsGeneration;
|
|
10125
10170
|
private currentRoomId;
|
|
10126
10171
|
private currentPeerId;
|
|
10127
10172
|
private currentUserId;
|
|
@@ -10158,6 +10203,14 @@ declare class WebRTCAPI {
|
|
|
10158
10203
|
*/
|
|
10159
10204
|
connect(options: WebRTCConnectOptions): Promise<void>;
|
|
10160
10205
|
private connectWebSocket;
|
|
10206
|
+
/**
|
|
10207
|
+
* 소켓에 붙은 핸들러를 전부 떼어낸다.
|
|
10208
|
+
*
|
|
10209
|
+
* `this.ws = null` 로 참조만 끊으면 클로저가 살아남아 close 이벤트가 계속 들어온다.
|
|
10210
|
+
* 세대 토큰이 그 이벤트를 무시해주긴 하지만, 핸들러 자체를 떼면 인스턴스 참조가 끊겨
|
|
10211
|
+
* 소켓이 GC 될 때까지 인스턴스를 붙잡고 있지 않는다.
|
|
10212
|
+
*/
|
|
10213
|
+
private static detachSocket;
|
|
10161
10214
|
private buildWebSocketUrl;
|
|
10162
10215
|
private handleSignalingMessage;
|
|
10163
10216
|
private createPeerConnection;
|
|
@@ -10172,6 +10225,44 @@ declare class WebRTCAPI {
|
|
|
10172
10225
|
* WebRTC 연결 해제
|
|
10173
10226
|
*/
|
|
10174
10227
|
disconnect(): void;
|
|
10228
|
+
/**
|
|
10229
|
+
* 현재 세션의 룸을 교체한다.
|
|
10230
|
+
*
|
|
10231
|
+
* `disconnect()` 후 곧바로 `connect()` 하는 패턴을 안전하게 감싼 것이다. 옛 소켓의
|
|
10232
|
+
* close 이벤트가 새 연결의 'connecting' 구간에 떨어져도 세대 토큰이 걸러낸다.
|
|
10233
|
+
* 생략한 옵션은 현재 세션 값을 그대로 이어받는다.
|
|
10234
|
+
*
|
|
10235
|
+
* @example
|
|
10236
|
+
* ```typescript
|
|
10237
|
+
* await cb.webrtc.switchRoom('channel:room-b')
|
|
10238
|
+
* ```
|
|
10239
|
+
*/
|
|
10240
|
+
switchRoom(roomId: string, options?: Omit<WebRTCConnectOptions, "roomId">): Promise<void>;
|
|
10241
|
+
/**
|
|
10242
|
+
* 소켓, 룸, 이벤트 리스너, 피어 연결을 **독립적으로** 갖는 새 WebRTC 세션을 만든다.
|
|
10243
|
+
*
|
|
10244
|
+
* `cb.webrtc` 는 인스턴스 하나에 소켓 하나/룸 하나라, 한 앱에 WebRTC 기능이 둘 이상이면
|
|
10245
|
+
* (예: 공간 음성채팅 + 1:1 통화) 서로 연결을 뺏고 이벤트가 교차한다. 기능마다 세션을
|
|
10246
|
+
* 하나씩 만들면 각자의 소켓과 핸들러를 갖는다.
|
|
10247
|
+
*
|
|
10248
|
+
* 인증 정보(HttpClient)와 서버 주소, appId 는 부모와 공유한다.
|
|
10249
|
+
*
|
|
10250
|
+
* @example
|
|
10251
|
+
* ```typescript
|
|
10252
|
+
* const voice = cb.webrtc.createSession()
|
|
10253
|
+
* const call = cb.webrtc.createSession()
|
|
10254
|
+
*
|
|
10255
|
+
* voice.onRemoteStream((peerId, stream) => attachToSpatialAudio(peerId, stream))
|
|
10256
|
+
* call.onRemoteStream((peerId, stream) => showCallScreen(stream))
|
|
10257
|
+
*
|
|
10258
|
+
* await voice.connect({ roomId: 'voice:lobby', localStream: mic })
|
|
10259
|
+
* await call.connect({ roomId: 'call:alice-bob', localStream: mic })
|
|
10260
|
+
*
|
|
10261
|
+
* // 각 세션은 독립적으로 끊는다
|
|
10262
|
+
* call.disconnect()
|
|
10263
|
+
* ```
|
|
10264
|
+
*/
|
|
10265
|
+
createSession(): WebRTCAPI;
|
|
10175
10266
|
/**
|
|
10176
10267
|
* 현재 연결 상태 조회
|
|
10177
10268
|
*/
|
package/dist/index.d.ts
CHANGED
|
@@ -329,6 +329,11 @@ declare class HttpClient {
|
|
|
329
329
|
private markSessionHint;
|
|
330
330
|
private clearSessionHint;
|
|
331
331
|
private hasSessionHint;
|
|
332
|
+
private buildNoCookieSessionKey;
|
|
333
|
+
private markNoCookieSession;
|
|
334
|
+
private clearNoCookieSessionMark;
|
|
335
|
+
/** 쿠키 전용 refresh 를 지금 건너뛰어야 하는지 (최근에 401/403 을 받았는지). */
|
|
336
|
+
private isNoCookieSessionMarkFresh;
|
|
332
337
|
/**
|
|
333
338
|
* OAuth redirect callback 직후 호출되어 HttpOnly cookie 를 부트스트랩한다.
|
|
334
339
|
*
|
|
@@ -7403,9 +7408,31 @@ declare class RealtimeAPI {
|
|
|
7403
7408
|
*/
|
|
7404
7409
|
getState(): ConnectionState;
|
|
7405
7410
|
/**
|
|
7406
|
-
*
|
|
7411
|
+
* pub/sub 사용 가능 여부.
|
|
7412
|
+
*
|
|
7413
|
+
* WebSocket 으로 연결됐을 때만 true 다. SSE fallback 모드에서는 `subscribe` /
|
|
7414
|
+
* `sendMessage` / `setPresence` / typing 이 전부 예외를 던지므로, 여기서 true 를
|
|
7415
|
+
* 반환하면 앱이 "연결됨"으로 오판하고 아무것도 오가지 않는 채 조용히 멈춘다
|
|
7416
|
+
* (platform-issue 01a01e6a-3cb1).
|
|
7417
|
+
*
|
|
7418
|
+
* AI 스트리밍만 필요하다면 {@link isStreamReady} 를, 다운그레이드 여부를 알고 싶다면
|
|
7419
|
+
* {@link isDegraded} 를 쓴다.
|
|
7407
7420
|
*/
|
|
7408
7421
|
isConnected(): boolean;
|
|
7422
|
+
/**
|
|
7423
|
+
* SSE fallback 으로 다운그레이드된 상태인지 여부.
|
|
7424
|
+
*
|
|
7425
|
+
* true 면 연결은 살아 있지만 단방향이라 `realtime.stream()` 만 동작한다. 사용자에게
|
|
7426
|
+
* "제한된 네트워크라 일부 기능을 쓸 수 없다" 고 안내할 지점이다.
|
|
7427
|
+
*/
|
|
7428
|
+
isDegraded(): boolean;
|
|
7429
|
+
/**
|
|
7430
|
+
* AI 스트리밍(`realtime.stream()`) 사용 가능 여부.
|
|
7431
|
+
*
|
|
7432
|
+
* WS 든 SSE fallback 이든 연결만 서 있으면 true 다. 종전 `isConnected()` 와 같은
|
|
7433
|
+
* 판정이므로, 스트리밍 게이팅에 `isConnected()` 를 쓰던 코드는 이쪽으로 옮긴다.
|
|
7434
|
+
*/
|
|
7435
|
+
isStreamReady(): boolean;
|
|
7409
7436
|
/**
|
|
7410
7437
|
* 상태 변경 핸들러 등록
|
|
7411
7438
|
*/
|
|
@@ -10047,6 +10074,14 @@ interface ICEServer {
|
|
|
10047
10074
|
/** ICE 서버 목록 응답 */
|
|
10048
10075
|
interface ICEServersResponse {
|
|
10049
10076
|
ice_servers: ICEServer[];
|
|
10077
|
+
/**
|
|
10078
|
+
* ephemeral TURN 자격증명의 만료 시각(Unix epoch 초).
|
|
10079
|
+
*
|
|
10080
|
+
* 서버가 `TURN_SHARED_SECRET` 으로 시간제한 자격증명을 발급한 경우에만 0 이 아니다.
|
|
10081
|
+
* STUN 전용 구성에서는 0 — 만료가 없다는 뜻이다. 통화가 길어질 수 있으면 이 시각
|
|
10082
|
+
* 전에 `getICEServers()` 를 다시 불러 새 자격증명을 받는다.
|
|
10083
|
+
*/
|
|
10084
|
+
expires_at?: number;
|
|
10050
10085
|
}
|
|
10051
10086
|
/**
|
|
10052
10087
|
* 채널별 통계 (룸 ID 의 `channel:room` 컨벤션 기준 집계).
|
|
@@ -10122,6 +10157,16 @@ declare class WebRTCAPI {
|
|
|
10122
10157
|
private reconnectAttempts;
|
|
10123
10158
|
private maxReconnectAttempts;
|
|
10124
10159
|
private reconnectTimeout;
|
|
10160
|
+
/**
|
|
10161
|
+
* 시그널링 소켓의 세대(generation) 토큰.
|
|
10162
|
+
*
|
|
10163
|
+
* 소켓을 열 때마다 1 증가하며, 각 핸들러는 자기가 열릴 때의 세대를 클로저에 담아둔다.
|
|
10164
|
+
* 세대가 밀린 핸들러는 "옛 소켓의 지연 이벤트" 이므로 인스턴스 상태를 건드리지 않는다.
|
|
10165
|
+
* 참조(`this.ws`)만 끊는 것으로는 부족하다 — 핸들러 클로저가 인스턴스를 캡처하고 있어
|
|
10166
|
+
* close 이벤트가 뒤늦게 도착하면 새 연결을 'disconnected' 로 덮어쓰고 새로 만든 피어
|
|
10167
|
+
* 연결까지 정리해버린다.
|
|
10168
|
+
*/
|
|
10169
|
+
private wsGeneration;
|
|
10125
10170
|
private currentRoomId;
|
|
10126
10171
|
private currentPeerId;
|
|
10127
10172
|
private currentUserId;
|
|
@@ -10158,6 +10203,14 @@ declare class WebRTCAPI {
|
|
|
10158
10203
|
*/
|
|
10159
10204
|
connect(options: WebRTCConnectOptions): Promise<void>;
|
|
10160
10205
|
private connectWebSocket;
|
|
10206
|
+
/**
|
|
10207
|
+
* 소켓에 붙은 핸들러를 전부 떼어낸다.
|
|
10208
|
+
*
|
|
10209
|
+
* `this.ws = null` 로 참조만 끊으면 클로저가 살아남아 close 이벤트가 계속 들어온다.
|
|
10210
|
+
* 세대 토큰이 그 이벤트를 무시해주긴 하지만, 핸들러 자체를 떼면 인스턴스 참조가 끊겨
|
|
10211
|
+
* 소켓이 GC 될 때까지 인스턴스를 붙잡고 있지 않는다.
|
|
10212
|
+
*/
|
|
10213
|
+
private static detachSocket;
|
|
10161
10214
|
private buildWebSocketUrl;
|
|
10162
10215
|
private handleSignalingMessage;
|
|
10163
10216
|
private createPeerConnection;
|
|
@@ -10172,6 +10225,44 @@ declare class WebRTCAPI {
|
|
|
10172
10225
|
* WebRTC 연결 해제
|
|
10173
10226
|
*/
|
|
10174
10227
|
disconnect(): void;
|
|
10228
|
+
/**
|
|
10229
|
+
* 현재 세션의 룸을 교체한다.
|
|
10230
|
+
*
|
|
10231
|
+
* `disconnect()` 후 곧바로 `connect()` 하는 패턴을 안전하게 감싼 것이다. 옛 소켓의
|
|
10232
|
+
* close 이벤트가 새 연결의 'connecting' 구간에 떨어져도 세대 토큰이 걸러낸다.
|
|
10233
|
+
* 생략한 옵션은 현재 세션 값을 그대로 이어받는다.
|
|
10234
|
+
*
|
|
10235
|
+
* @example
|
|
10236
|
+
* ```typescript
|
|
10237
|
+
* await cb.webrtc.switchRoom('channel:room-b')
|
|
10238
|
+
* ```
|
|
10239
|
+
*/
|
|
10240
|
+
switchRoom(roomId: string, options?: Omit<WebRTCConnectOptions, "roomId">): Promise<void>;
|
|
10241
|
+
/**
|
|
10242
|
+
* 소켓, 룸, 이벤트 리스너, 피어 연결을 **독립적으로** 갖는 새 WebRTC 세션을 만든다.
|
|
10243
|
+
*
|
|
10244
|
+
* `cb.webrtc` 는 인스턴스 하나에 소켓 하나/룸 하나라, 한 앱에 WebRTC 기능이 둘 이상이면
|
|
10245
|
+
* (예: 공간 음성채팅 + 1:1 통화) 서로 연결을 뺏고 이벤트가 교차한다. 기능마다 세션을
|
|
10246
|
+
* 하나씩 만들면 각자의 소켓과 핸들러를 갖는다.
|
|
10247
|
+
*
|
|
10248
|
+
* 인증 정보(HttpClient)와 서버 주소, appId 는 부모와 공유한다.
|
|
10249
|
+
*
|
|
10250
|
+
* @example
|
|
10251
|
+
* ```typescript
|
|
10252
|
+
* const voice = cb.webrtc.createSession()
|
|
10253
|
+
* const call = cb.webrtc.createSession()
|
|
10254
|
+
*
|
|
10255
|
+
* voice.onRemoteStream((peerId, stream) => attachToSpatialAudio(peerId, stream))
|
|
10256
|
+
* call.onRemoteStream((peerId, stream) => showCallScreen(stream))
|
|
10257
|
+
*
|
|
10258
|
+
* await voice.connect({ roomId: 'voice:lobby', localStream: mic })
|
|
10259
|
+
* await call.connect({ roomId: 'call:alice-bob', localStream: mic })
|
|
10260
|
+
*
|
|
10261
|
+
* // 각 세션은 독립적으로 끊는다
|
|
10262
|
+
* call.disconnect()
|
|
10263
|
+
* ```
|
|
10264
|
+
*/
|
|
10265
|
+
createSession(): WebRTCAPI;
|
|
10175
10266
|
/**
|
|
10176
10267
|
* 현재 연결 상태 조회
|
|
10177
10268
|
*/
|
package/dist/index.js
CHANGED
|
@@ -7936,9 +7936,35 @@ var RealtimeAPI = class {
|
|
|
7936
7936
|
return this.state;
|
|
7937
7937
|
}
|
|
7938
7938
|
/**
|
|
7939
|
-
*
|
|
7939
|
+
* pub/sub 사용 가능 여부.
|
|
7940
|
+
*
|
|
7941
|
+
* WebSocket 으로 연결됐을 때만 true 다. SSE fallback 모드에서는 `subscribe` /
|
|
7942
|
+
* `sendMessage` / `setPresence` / typing 이 전부 예외를 던지므로, 여기서 true 를
|
|
7943
|
+
* 반환하면 앱이 "연결됨"으로 오판하고 아무것도 오가지 않는 채 조용히 멈춘다
|
|
7944
|
+
* (platform-issue 01a01e6a-3cb1).
|
|
7945
|
+
*
|
|
7946
|
+
* AI 스트리밍만 필요하다면 {@link isStreamReady} 를, 다운그레이드 여부를 알고 싶다면
|
|
7947
|
+
* {@link isDegraded} 를 쓴다.
|
|
7940
7948
|
*/
|
|
7941
7949
|
isConnected() {
|
|
7950
|
+
return this.state === "connected" && this.activeTransport === "ws";
|
|
7951
|
+
}
|
|
7952
|
+
/**
|
|
7953
|
+
* SSE fallback 으로 다운그레이드된 상태인지 여부.
|
|
7954
|
+
*
|
|
7955
|
+
* true 면 연결은 살아 있지만 단방향이라 `realtime.stream()` 만 동작한다. 사용자에게
|
|
7956
|
+
* "제한된 네트워크라 일부 기능을 쓸 수 없다" 고 안내할 지점이다.
|
|
7957
|
+
*/
|
|
7958
|
+
isDegraded() {
|
|
7959
|
+
return this.sseFallbackActive;
|
|
7960
|
+
}
|
|
7961
|
+
/**
|
|
7962
|
+
* AI 스트리밍(`realtime.stream()`) 사용 가능 여부.
|
|
7963
|
+
*
|
|
7964
|
+
* WS 든 SSE fallback 이든 연결만 서 있으면 true 다. 종전 `isConnected()` 와 같은
|
|
7965
|
+
* 판정이므로, 스트리밍 게이팅에 `isConnected()` 를 쓰던 코드는 이쪽으로 옮긴다.
|
|
7966
|
+
*/
|
|
7967
|
+
isStreamReady() {
|
|
7942
7968
|
return this.state === "connected";
|
|
7943
7969
|
}
|
|
7944
7970
|
/**
|
|
@@ -11069,7 +11095,7 @@ var VideoAPI = class {
|
|
|
11069
11095
|
};
|
|
11070
11096
|
|
|
11071
11097
|
// src/api/webrtc.ts
|
|
11072
|
-
var WebRTCAPI = class {
|
|
11098
|
+
var WebRTCAPI = class _WebRTCAPI {
|
|
11073
11099
|
constructor(http, webrtcUrl, appId) {
|
|
11074
11100
|
this.ws = null;
|
|
11075
11101
|
this.state = "disconnected";
|
|
@@ -11081,6 +11107,16 @@ var WebRTCAPI = class {
|
|
|
11081
11107
|
this.reconnectAttempts = 0;
|
|
11082
11108
|
this.maxReconnectAttempts = 5;
|
|
11083
11109
|
this.reconnectTimeout = null;
|
|
11110
|
+
/**
|
|
11111
|
+
* 시그널링 소켓의 세대(generation) 토큰.
|
|
11112
|
+
*
|
|
11113
|
+
* 소켓을 열 때마다 1 증가하며, 각 핸들러는 자기가 열릴 때의 세대를 클로저에 담아둔다.
|
|
11114
|
+
* 세대가 밀린 핸들러는 "옛 소켓의 지연 이벤트" 이므로 인스턴스 상태를 건드리지 않는다.
|
|
11115
|
+
* 참조(`this.ws`)만 끊는 것으로는 부족하다 — 핸들러 클로저가 인스턴스를 캡처하고 있어
|
|
11116
|
+
* close 이벤트가 뒤늦게 도착하면 새 연결을 'disconnected' 로 덮어쓰고 새로 만든 피어
|
|
11117
|
+
* 연결까지 정리해버린다.
|
|
11118
|
+
*/
|
|
11119
|
+
this.wsGeneration = 0;
|
|
11084
11120
|
// 현재 연결 정보
|
|
11085
11121
|
this.currentRoomId = null;
|
|
11086
11122
|
this.currentPeerId = null;
|
|
@@ -11153,15 +11189,23 @@ var WebRTCAPI = class {
|
|
|
11153
11189
|
connectWebSocket() {
|
|
11154
11190
|
return new Promise((resolve, reject) => {
|
|
11155
11191
|
const wsUrl = this.buildWebSocketUrl();
|
|
11156
|
-
|
|
11192
|
+
const generation = ++this.wsGeneration;
|
|
11193
|
+
const ws = new WebSocket(wsUrl);
|
|
11194
|
+
this.ws = ws;
|
|
11195
|
+
const isStale = () => this.wsGeneration !== generation;
|
|
11157
11196
|
const timeout = setTimeout(() => {
|
|
11197
|
+
if (isStale()) return;
|
|
11158
11198
|
if (this.state === "connecting") {
|
|
11159
|
-
|
|
11199
|
+
ws.close();
|
|
11160
11200
|
reject(new Error("\uC5F0\uACB0 \uC2DC\uAC04 \uCD08\uACFC"));
|
|
11161
11201
|
}
|
|
11162
11202
|
}, 1e4);
|
|
11163
|
-
|
|
11203
|
+
ws.onopen = () => {
|
|
11164
11204
|
clearTimeout(timeout);
|
|
11205
|
+
if (isStale()) {
|
|
11206
|
+
ws.close(1e3, "superseded");
|
|
11207
|
+
return;
|
|
11208
|
+
}
|
|
11165
11209
|
this.reconnectAttempts = 0;
|
|
11166
11210
|
this.sendSignaling({
|
|
11167
11211
|
type: "join",
|
|
@@ -11172,7 +11216,8 @@ var WebRTCAPI = class {
|
|
|
11172
11216
|
}
|
|
11173
11217
|
});
|
|
11174
11218
|
};
|
|
11175
|
-
|
|
11219
|
+
ws.onmessage = async (event) => {
|
|
11220
|
+
if (isStale()) return;
|
|
11176
11221
|
try {
|
|
11177
11222
|
const msg = JSON.parse(event.data);
|
|
11178
11223
|
await this.handleSignalingMessage(msg, resolve, reject);
|
|
@@ -11180,13 +11225,15 @@ var WebRTCAPI = class {
|
|
|
11180
11225
|
console.error("Failed to parse signaling message:", error);
|
|
11181
11226
|
}
|
|
11182
11227
|
};
|
|
11183
|
-
|
|
11228
|
+
ws.onerror = (event) => {
|
|
11184
11229
|
clearTimeout(timeout);
|
|
11230
|
+
if (isStale()) return;
|
|
11185
11231
|
console.error("WebSocket error:", event);
|
|
11186
11232
|
this.emitError(new Error("WebSocket \uC5F0\uACB0 \uC624\uB958"));
|
|
11187
11233
|
};
|
|
11188
|
-
|
|
11234
|
+
ws.onclose = (event) => {
|
|
11189
11235
|
clearTimeout(timeout);
|
|
11236
|
+
if (isStale()) return;
|
|
11190
11237
|
if (this.state === "connecting") {
|
|
11191
11238
|
reject(new Error("\uC5F0\uACB0\uC774 \uC885\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4"));
|
|
11192
11239
|
}
|
|
@@ -11194,6 +11241,19 @@ var WebRTCAPI = class {
|
|
|
11194
11241
|
};
|
|
11195
11242
|
});
|
|
11196
11243
|
}
|
|
11244
|
+
/**
|
|
11245
|
+
* 소켓에 붙은 핸들러를 전부 떼어낸다.
|
|
11246
|
+
*
|
|
11247
|
+
* `this.ws = null` 로 참조만 끊으면 클로저가 살아남아 close 이벤트가 계속 들어온다.
|
|
11248
|
+
* 세대 토큰이 그 이벤트를 무시해주긴 하지만, 핸들러 자체를 떼면 인스턴스 참조가 끊겨
|
|
11249
|
+
* 소켓이 GC 될 때까지 인스턴스를 붙잡고 있지 않는다.
|
|
11250
|
+
*/
|
|
11251
|
+
static detachSocket(ws) {
|
|
11252
|
+
ws.onopen = null;
|
|
11253
|
+
ws.onmessage = null;
|
|
11254
|
+
ws.onerror = null;
|
|
11255
|
+
ws.onclose = null;
|
|
11256
|
+
}
|
|
11197
11257
|
buildWebSocketUrl() {
|
|
11198
11258
|
const wsBase = this.webrtcUrl.replace("https://", "wss://").replace("http://", "ws://");
|
|
11199
11259
|
const publicKey = this.http.getPublicKey();
|
|
@@ -11404,19 +11464,76 @@ var WebRTCAPI = class {
|
|
|
11404
11464
|
clearTimeout(this.reconnectTimeout);
|
|
11405
11465
|
this.reconnectTimeout = null;
|
|
11406
11466
|
}
|
|
11407
|
-
|
|
11408
|
-
|
|
11409
|
-
|
|
11467
|
+
this.reconnectAttempts = 0;
|
|
11468
|
+
this.wsGeneration++;
|
|
11469
|
+
const ws = this.ws;
|
|
11470
|
+
this.ws = null;
|
|
11471
|
+
if (ws) {
|
|
11472
|
+
_WebRTCAPI.detachSocket(ws);
|
|
11473
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
11474
|
+
try {
|
|
11475
|
+
ws.send(JSON.stringify({ type: "leave" }));
|
|
11476
|
+
} catch {
|
|
11477
|
+
}
|
|
11478
|
+
ws.close(1e3, "User disconnected");
|
|
11479
|
+
} else if (ws.readyState === WebSocket.CONNECTING) {
|
|
11480
|
+
ws.close();
|
|
11481
|
+
}
|
|
11410
11482
|
}
|
|
11411
11483
|
this.peerConnections.forEach((pc) => pc.close());
|
|
11412
11484
|
this.peerConnections.clear();
|
|
11413
11485
|
this.remoteStreams.clear();
|
|
11414
|
-
this.ws = null;
|
|
11415
11486
|
this.currentRoomId = null;
|
|
11416
11487
|
this.currentPeerId = null;
|
|
11417
11488
|
this.localStream = null;
|
|
11418
11489
|
this.setState("disconnected");
|
|
11419
11490
|
}
|
|
11491
|
+
/**
|
|
11492
|
+
* 현재 세션의 룸을 교체한다.
|
|
11493
|
+
*
|
|
11494
|
+
* `disconnect()` 후 곧바로 `connect()` 하는 패턴을 안전하게 감싼 것이다. 옛 소켓의
|
|
11495
|
+
* close 이벤트가 새 연결의 'connecting' 구간에 떨어져도 세대 토큰이 걸러낸다.
|
|
11496
|
+
* 생략한 옵션은 현재 세션 값을 그대로 이어받는다.
|
|
11497
|
+
*
|
|
11498
|
+
* @example
|
|
11499
|
+
* ```typescript
|
|
11500
|
+
* await cb.webrtc.switchRoom('channel:room-b')
|
|
11501
|
+
* ```
|
|
11502
|
+
*/
|
|
11503
|
+
async switchRoom(roomId, options = {}) {
|
|
11504
|
+
const localStream = options.localStream ?? this.localStream ?? void 0;
|
|
11505
|
+
const userId = options.userId ?? this.currentUserId ?? void 0;
|
|
11506
|
+
const isBroadcaster = options.isBroadcaster ?? this.isBroadcaster;
|
|
11507
|
+
this.disconnect();
|
|
11508
|
+
await this.connect({ roomId, userId, isBroadcaster, localStream });
|
|
11509
|
+
}
|
|
11510
|
+
/**
|
|
11511
|
+
* 소켓, 룸, 이벤트 리스너, 피어 연결을 **독립적으로** 갖는 새 WebRTC 세션을 만든다.
|
|
11512
|
+
*
|
|
11513
|
+
* `cb.webrtc` 는 인스턴스 하나에 소켓 하나/룸 하나라, 한 앱에 WebRTC 기능이 둘 이상이면
|
|
11514
|
+
* (예: 공간 음성채팅 + 1:1 통화) 서로 연결을 뺏고 이벤트가 교차한다. 기능마다 세션을
|
|
11515
|
+
* 하나씩 만들면 각자의 소켓과 핸들러를 갖는다.
|
|
11516
|
+
*
|
|
11517
|
+
* 인증 정보(HttpClient)와 서버 주소, appId 는 부모와 공유한다.
|
|
11518
|
+
*
|
|
11519
|
+
* @example
|
|
11520
|
+
* ```typescript
|
|
11521
|
+
* const voice = cb.webrtc.createSession()
|
|
11522
|
+
* const call = cb.webrtc.createSession()
|
|
11523
|
+
*
|
|
11524
|
+
* voice.onRemoteStream((peerId, stream) => attachToSpatialAudio(peerId, stream))
|
|
11525
|
+
* call.onRemoteStream((peerId, stream) => showCallScreen(stream))
|
|
11526
|
+
*
|
|
11527
|
+
* await voice.connect({ roomId: 'voice:lobby', localStream: mic })
|
|
11528
|
+
* await call.connect({ roomId: 'call:alice-bob', localStream: mic })
|
|
11529
|
+
*
|
|
11530
|
+
* // 각 세션은 독립적으로 끊는다
|
|
11531
|
+
* call.disconnect()
|
|
11532
|
+
* ```
|
|
11533
|
+
*/
|
|
11534
|
+
createSession() {
|
|
11535
|
+
return new _WebRTCAPI(this.http, this.webrtcUrl, this.appId);
|
|
11536
|
+
}
|
|
11420
11537
|
/**
|
|
11421
11538
|
* 현재 연결 상태 조회
|
|
11422
11539
|
*/
|
|
@@ -11661,6 +11778,7 @@ function fetchCredentialsForPath(url) {
|
|
|
11661
11778
|
return path.startsWith("/v1/public/") || path.startsWith("/v1/proxy/") ? "omit" : "include";
|
|
11662
11779
|
}
|
|
11663
11780
|
var TOKEN_STORAGE_KEY = "cb_auth_tokens";
|
|
11781
|
+
var NO_COOKIE_SESSION_COOLDOWN_MS = 6e4;
|
|
11664
11782
|
function gatewayCodeFromStatus(status) {
|
|
11665
11783
|
switch (status) {
|
|
11666
11784
|
case 502:
|
|
@@ -11805,6 +11923,7 @@ var HttpClient = class {
|
|
|
11805
11923
|
this.config.refreshToken = refreshToken;
|
|
11806
11924
|
this.persistTokens();
|
|
11807
11925
|
this.markSessionHint();
|
|
11926
|
+
this.clearNoCookieSessionMark();
|
|
11808
11927
|
}
|
|
11809
11928
|
clearTokens() {
|
|
11810
11929
|
this.config.accessToken = void 0;
|
|
@@ -11848,6 +11967,45 @@ var HttpClient = class {
|
|
|
11848
11967
|
return true;
|
|
11849
11968
|
}
|
|
11850
11969
|
}
|
|
11970
|
+
// ===== cookie 세션 없음 마커 (platform-issue 01a0237f) =====
|
|
11971
|
+
//
|
|
11972
|
+
// `refreshLockedUntil` 백오프는 인스턴스 안에서만 산다. 앱이 클라이언트를 새로 만들 때마다
|
|
11973
|
+
// 초기화되므로, 쿠키가 없는 브라우저에서는 인증 호출마다 re-issue 401 이 새로 나간다.
|
|
11974
|
+
// 이 마커는 같은 사실을 localStorage 에 짧게 남겨 인스턴스 경계를 넘어 공유한다.
|
|
11975
|
+
buildNoCookieSessionKey() {
|
|
11976
|
+
return `${this.storageKey}:no_cookie_session`;
|
|
11977
|
+
}
|
|
11978
|
+
markNoCookieSession() {
|
|
11979
|
+
if (typeof window === "undefined") return;
|
|
11980
|
+
try {
|
|
11981
|
+
localStorage.setItem(this.buildNoCookieSessionKey(), String(Date.now()));
|
|
11982
|
+
} catch {
|
|
11983
|
+
}
|
|
11984
|
+
}
|
|
11985
|
+
clearNoCookieSessionMark() {
|
|
11986
|
+
if (typeof window === "undefined") return;
|
|
11987
|
+
try {
|
|
11988
|
+
localStorage.removeItem(this.buildNoCookieSessionKey());
|
|
11989
|
+
} catch {
|
|
11990
|
+
}
|
|
11991
|
+
}
|
|
11992
|
+
/** 쿠키 전용 refresh 를 지금 건너뛰어야 하는지 (최근에 401/403 을 받았는지). */
|
|
11993
|
+
isNoCookieSessionMarkFresh() {
|
|
11994
|
+
if (typeof window === "undefined") return false;
|
|
11995
|
+
try {
|
|
11996
|
+
const raw = localStorage.getItem(this.buildNoCookieSessionKey());
|
|
11997
|
+
if (!raw) return false;
|
|
11998
|
+
const at = Number.parseInt(raw, 10);
|
|
11999
|
+
if (!Number.isFinite(at)) return false;
|
|
12000
|
+
if (Date.now() - at >= NO_COOKIE_SESSION_COOLDOWN_MS) {
|
|
12001
|
+
this.clearNoCookieSessionMark();
|
|
12002
|
+
return false;
|
|
12003
|
+
}
|
|
12004
|
+
return true;
|
|
12005
|
+
} catch {
|
|
12006
|
+
return false;
|
|
12007
|
+
}
|
|
12008
|
+
}
|
|
11851
12009
|
/**
|
|
11852
12010
|
* OAuth redirect callback 직후 호출되어 HttpOnly cookie 를 부트스트랩한다.
|
|
11853
12011
|
*
|
|
@@ -12033,6 +12191,11 @@ var HttpClient = class {
|
|
|
12033
12191
|
this.config.onAuthError?.(error);
|
|
12034
12192
|
throw error;
|
|
12035
12193
|
}
|
|
12194
|
+
if (!this.config.refreshToken && this.isNoCookieSessionMarkFresh()) {
|
|
12195
|
+
throw new AuthError(
|
|
12196
|
+
"No cookie session (recently rejected). Skipping token refresh."
|
|
12197
|
+
);
|
|
12198
|
+
}
|
|
12036
12199
|
this.isRefreshing = true;
|
|
12037
12200
|
if (!this.config.refreshToken && typeof window === "undefined") {
|
|
12038
12201
|
this.isRefreshing = false;
|
|
@@ -12124,6 +12287,7 @@ var HttpClient = class {
|
|
|
12124
12287
|
});
|
|
12125
12288
|
this.refreshFailureCount = 0;
|
|
12126
12289
|
this.refreshLockedUntil = 0;
|
|
12290
|
+
this.clearNoCookieSessionMark();
|
|
12127
12291
|
return data.access_token;
|
|
12128
12292
|
} catch (e) {
|
|
12129
12293
|
const baseMsg = e instanceof Error ? e.message : "Token refresh failed";
|
|
@@ -12133,6 +12297,9 @@ var HttpClient = class {
|
|
|
12133
12297
|
3e4
|
|
12134
12298
|
);
|
|
12135
12299
|
this.refreshLockedUntil = Date.now() + backoffMs;
|
|
12300
|
+
if (failureKind === "permanent" && !this.config.refreshToken) {
|
|
12301
|
+
this.markNoCookieSession();
|
|
12302
|
+
}
|
|
12136
12303
|
if (failureKind === "permanent") {
|
|
12137
12304
|
if (!silent) {
|
|
12138
12305
|
this.clearTokens();
|