connectbase-client 6.14.0 → 6.16.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 +44 -0
- package/README.md +31 -1
- package/dist/cli.js +1 -1
- package/dist/connect-base.umd.js +5 -5
- package/dist/index.d.mts +102 -5
- package/dist/index.d.ts +102 -5
- package/dist/index.js +143 -24
- package/dist/index.mjs +143 -24
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -291,7 +291,7 @@ var RealtimeConnectionError = class extends Error {
|
|
|
291
291
|
this.name = "RealtimeConnectionError";
|
|
292
292
|
this.code = init.code || "unknown";
|
|
293
293
|
this.closeCode = init.closeCode;
|
|
294
|
-
this.retryable = init.retryable ?? isRetryableRealtimeRejection(this.code);
|
|
294
|
+
this.retryable = init.retryable ?? isRetryableRealtimeRejection(this.code, init.closeCode);
|
|
295
295
|
this.details = init.details;
|
|
296
296
|
this.retryAfterMs = init.retryAfterMs;
|
|
297
297
|
}
|
|
@@ -308,8 +308,8 @@ var RealtimeConnectionError = class extends Error {
|
|
|
308
308
|
};
|
|
309
309
|
}
|
|
310
310
|
};
|
|
311
|
-
function isRetryableRealtimeRejection(code) {
|
|
312
|
-
return code === "connection_limit_exceeded" || code === "server_at_capacity" || code === "rate_limited" || code === "auth_unavailable" || code === "internal_error" || code === "invalid_ws_ticket" || code === "ws_ticket_unavailable";
|
|
311
|
+
function isRetryableRealtimeRejection(code, closeCode) {
|
|
312
|
+
return closeCode === 4429 || closeCode === 4500 || closeCode === 4503 || code === "connection_limit_exceeded" || code === "rate_limit_exceeded" || code === "server_at_capacity" || code === "rate_limited" || code === "auth_unavailable" || code === "internal_error" || code === "invalid_ws_ticket" || code === "ws_ticket_unavailable";
|
|
313
313
|
}
|
|
314
314
|
var RealtimeRequestError = class extends Error {
|
|
315
315
|
constructor(init) {
|
|
@@ -3807,6 +3807,20 @@ var REALTIME_CLOSING_NOTICE_CODES = /* @__PURE__ */ new Set([
|
|
|
3807
3807
|
]);
|
|
3808
3808
|
var CLOSE_CODE_TOKEN_EXPIRED = 4401;
|
|
3809
3809
|
var CLOSE_CODE_MEMBER_REVOKED = 4403;
|
|
3810
|
+
function toRealtimeRejection(msg) {
|
|
3811
|
+
if (msg.type !== "error" || msg.request_id || typeof msg.close_code !== "number") {
|
|
3812
|
+
return null;
|
|
3813
|
+
}
|
|
3814
|
+
const details = msg.details;
|
|
3815
|
+
return new RealtimeConnectionError({
|
|
3816
|
+
code: typeof msg.code === "string" ? msg.code : void 0,
|
|
3817
|
+
message: typeof msg.message === "string" ? msg.message : void 0,
|
|
3818
|
+
closeCode: msg.close_code,
|
|
3819
|
+
retryable: typeof msg.retryable === "boolean" ? msg.retryable : void 0,
|
|
3820
|
+
details: details && typeof details === "object" && !Array.isArray(details) ? details : void 0,
|
|
3821
|
+
retryAfterMs: typeof msg.retry_after_ms === "number" && msg.retry_after_ms > 0 ? msg.retry_after_ms : void 0
|
|
3822
|
+
});
|
|
3823
|
+
}
|
|
3810
3824
|
var DatabaseAPI = class {
|
|
3811
3825
|
constructor(http) {
|
|
3812
3826
|
this.realtimeWs = null;
|
|
@@ -4749,18 +4763,13 @@ var DatabaseAPI = class {
|
|
|
4749
4763
|
ticket = await this.realtimeTicket();
|
|
4750
4764
|
} catch (e) {
|
|
4751
4765
|
if (e instanceof WsTicketError) {
|
|
4752
|
-
if (e.status === 429 && this.realtimeOptions) {
|
|
4753
|
-
this.attemptRealtimeReconnect();
|
|
4754
|
-
} else {
|
|
4755
|
-
this.setRealtimeState("disconnected");
|
|
4756
|
-
}
|
|
4757
4766
|
const err = new RealtimeConnectionError({
|
|
4758
4767
|
code: e.code,
|
|
4759
4768
|
message: e.message,
|
|
4760
4769
|
closeCode: 4e3 + e.status,
|
|
4761
4770
|
retryAfterMs: e.retryAfterMs
|
|
4762
4771
|
});
|
|
4763
|
-
this.
|
|
4772
|
+
this.handleRealtimeRejection(err);
|
|
4764
4773
|
throw err;
|
|
4765
4774
|
}
|
|
4766
4775
|
throw e;
|
|
@@ -4794,10 +4803,14 @@ var DatabaseAPI = class {
|
|
|
4794
4803
|
this.realtimeOptions.accessToken
|
|
4795
4804
|
);
|
|
4796
4805
|
if (this.realtimeTokenRefresh) url += "&token_refresh=1";
|
|
4806
|
+
url += "&reject_mode=close";
|
|
4797
4807
|
return new Promise((resolve, reject) => {
|
|
4798
4808
|
try {
|
|
4799
|
-
|
|
4809
|
+
const ws = new WebSocket(url);
|
|
4810
|
+
this.realtimeWs = ws;
|
|
4800
4811
|
let settled = false;
|
|
4812
|
+
let accepted = false;
|
|
4813
|
+
let rejection = null;
|
|
4801
4814
|
const connectTimeout = setTimeout(() => {
|
|
4802
4815
|
if (!settled) {
|
|
4803
4816
|
settled = true;
|
|
@@ -4809,7 +4822,8 @@ var DatabaseAPI = class {
|
|
|
4809
4822
|
reject(new Error("Connection timeout"));
|
|
4810
4823
|
}
|
|
4811
4824
|
}, REALTIME_CONNECT_TIMEOUT_MS);
|
|
4812
|
-
|
|
4825
|
+
const accept = () => {
|
|
4826
|
+
accepted = true;
|
|
4813
4827
|
if (!settled) {
|
|
4814
4828
|
settled = true;
|
|
4815
4829
|
clearTimeout(connectTimeout);
|
|
@@ -4822,15 +4836,28 @@ var DatabaseAPI = class {
|
|
|
4822
4836
|
this.resubscribeAll();
|
|
4823
4837
|
resolve();
|
|
4824
4838
|
};
|
|
4825
|
-
|
|
4839
|
+
ws.onopen = () => {
|
|
4840
|
+
ws.send(JSON.stringify({ type: "ping", timestamp: Date.now() }));
|
|
4841
|
+
};
|
|
4842
|
+
ws.onmessage = (event) => {
|
|
4843
|
+
let msg;
|
|
4826
4844
|
try {
|
|
4827
|
-
|
|
4828
|
-
this.handleRealtimeMessage(msg);
|
|
4845
|
+
msg = JSON.parse(event.data);
|
|
4829
4846
|
} catch {
|
|
4830
4847
|
this.debugLog("Failed to parse realtime message");
|
|
4848
|
+
return;
|
|
4849
|
+
}
|
|
4850
|
+
if (!accepted) {
|
|
4851
|
+
const rejected = toRealtimeRejection(msg);
|
|
4852
|
+
if (rejected) {
|
|
4853
|
+
rejection = rejected;
|
|
4854
|
+
return;
|
|
4855
|
+
}
|
|
4856
|
+
accept();
|
|
4831
4857
|
}
|
|
4858
|
+
this.handleRealtimeMessage(msg);
|
|
4832
4859
|
};
|
|
4833
|
-
|
|
4860
|
+
ws.onclose = (event) => {
|
|
4834
4861
|
this.debugLog(
|
|
4835
4862
|
`Database realtime disconnected (code=${event?.code}, reason=${event?.reason})`
|
|
4836
4863
|
);
|
|
@@ -4839,12 +4866,25 @@ var DatabaseAPI = class {
|
|
|
4839
4866
|
this.clearRealtimeAuthRetry();
|
|
4840
4867
|
const notice = this.realtimeCloseNotice;
|
|
4841
4868
|
this.realtimeCloseNotice = null;
|
|
4869
|
+
const code = event?.code;
|
|
4870
|
+
if (!accepted && code >= 4e3 && code <= 4999) {
|
|
4871
|
+
const err = rejection ?? new RealtimeConnectionError({
|
|
4872
|
+
code: event.reason || void 0,
|
|
4873
|
+
closeCode: code
|
|
4874
|
+
});
|
|
4875
|
+
if (!settled) {
|
|
4876
|
+
settled = true;
|
|
4877
|
+
clearTimeout(connectTimeout);
|
|
4878
|
+
reject(err);
|
|
4879
|
+
}
|
|
4880
|
+
this.handleRealtimeRejection(err);
|
|
4881
|
+
return;
|
|
4882
|
+
}
|
|
4842
4883
|
if (!settled) {
|
|
4843
4884
|
settled = true;
|
|
4844
4885
|
clearTimeout(connectTimeout);
|
|
4845
4886
|
reject(new Error("Connection closed during handshake"));
|
|
4846
4887
|
}
|
|
4847
|
-
const code = event?.code;
|
|
4848
4888
|
if (this.realtimeOptions && this.realtimeState !== "disconnected" && (code === CLOSE_CODE_MEMBER_REVOKED || code === CLOSE_CODE_TOKEN_EXPIRED && !this.http.canRefreshMemberToken(
|
|
4849
4889
|
this.realtimeOptions.accessToken
|
|
4850
4890
|
))) {
|
|
@@ -4862,7 +4902,7 @@ var DatabaseAPI = class {
|
|
|
4862
4902
|
this.attemptRealtimeReconnect();
|
|
4863
4903
|
}
|
|
4864
4904
|
};
|
|
4865
|
-
|
|
4905
|
+
ws.onerror = () => {
|
|
4866
4906
|
this.debugLog("Database realtime error");
|
|
4867
4907
|
this.realtimeOnError?.(new Error("WebSocket connection error"));
|
|
4868
4908
|
};
|
|
@@ -4872,6 +4912,21 @@ var DatabaseAPI = class {
|
|
|
4872
4912
|
}
|
|
4873
4913
|
});
|
|
4874
4914
|
}
|
|
4915
|
+
/**
|
|
4916
|
+
* 서버가 연결을 거부했다(티켓 발급 거부, 또는 reject_mode=close 로 받은 업그레이드 거부). cb.realtime 과 같은 규칙이다 —
|
|
4917
|
+
* 다시 해서 될 거부(`retryable`: 일시 장애, 속도 제한, 동시접속 한도, 만료된 티켓)는 서버가 알려 준 시각(`retryAfterMs`)
|
|
4918
|
+
* 이후로 재연결을 이어 가고, 아니면(자격증명, 멤버 폐기, 할당량) 멈추고 `onRealtimeError` 로 알린다. 재시도를 다 써도
|
|
4919
|
+
* 거부되면 마지막 거부를 알린다.
|
|
4920
|
+
*/
|
|
4921
|
+
handleRealtimeRejection(err) {
|
|
4922
|
+
if (!this.realtimeOptions || this.realtimeState === "disconnected") return;
|
|
4923
|
+
if (err.retryable) {
|
|
4924
|
+
this.attemptRealtimeReconnect(err);
|
|
4925
|
+
return;
|
|
4926
|
+
}
|
|
4927
|
+
this.setRealtimeState("disconnected");
|
|
4928
|
+
this.realtimeOnError?.(err);
|
|
4929
|
+
}
|
|
4875
4930
|
/**
|
|
4876
4931
|
* 서버에 subscribe 메시지를 보내고 서버 subscription_id를 반환하는 Promise
|
|
4877
4932
|
*/
|
|
@@ -5044,21 +5099,25 @@ var DatabaseAPI = class {
|
|
|
5044
5099
|
break;
|
|
5045
5100
|
}
|
|
5046
5101
|
}
|
|
5047
|
-
|
|
5102
|
+
/**
|
|
5103
|
+
* 지수 백오프로 다시 붙는다. cause 는 이번 시도를 끝낸 서버의 거부다 — 알려 준 재시도 시각(`retryAfterMs`)보다 일찍 다시
|
|
5104
|
+
* 하지 않고, 재시도를 다 쓰면 일반 오류 대신 그 거부를 알린다.
|
|
5105
|
+
*/
|
|
5106
|
+
attemptRealtimeReconnect(cause) {
|
|
5048
5107
|
const maxRetries = this.realtimeOptions?.maxRetries ?? 5;
|
|
5049
5108
|
const retryInterval = this.realtimeOptions?.retryInterval ?? 1e3;
|
|
5050
5109
|
if (this.realtimeRetryCount >= maxRetries) {
|
|
5051
5110
|
this.setRealtimeState("disconnected");
|
|
5052
5111
|
this.realtimeOnError?.(
|
|
5053
|
-
new Error("Realtime connection lost. Max retries exceeded.")
|
|
5112
|
+
cause ?? new Error("Realtime connection lost. Max retries exceeded.")
|
|
5054
5113
|
);
|
|
5055
5114
|
return;
|
|
5056
5115
|
}
|
|
5057
5116
|
this.setRealtimeState("connecting");
|
|
5058
5117
|
this.realtimeRetryCount++;
|
|
5059
|
-
const delay = Math.
|
|
5060
|
-
retryInterval * 2 ** (this.realtimeRetryCount - 1),
|
|
5061
|
-
|
|
5118
|
+
const delay = Math.max(
|
|
5119
|
+
Math.min(retryInterval * 2 ** (this.realtimeRetryCount - 1), 3e4),
|
|
5120
|
+
cause?.retryAfterMs ?? 0
|
|
5062
5121
|
);
|
|
5063
5122
|
this.debugLog(
|
|
5064
5123
|
`Reconnecting in ${delay}ms (attempt ${this.realtimeRetryCount}/${maxRetries})`
|
|
@@ -12563,6 +12622,29 @@ var StorageAPI = class {
|
|
|
12563
12622
|
getFileUrl(file) {
|
|
12564
12623
|
return file.url || null;
|
|
12565
12624
|
}
|
|
12625
|
+
/**
|
|
12626
|
+
* 경로 주소 만들기 — 파일을 경로로 읽는 고정 주소 (요청을 보내지 않는다)
|
|
12627
|
+
*
|
|
12628
|
+
* 돌려주는 주소는 그 경로의 파일로 리다이렉트된다. 인증 헤더가 필요 없어 `<img src>`, `fetch()`,
|
|
12629
|
+
* 기준 주소 + 상대 경로 로딩에 쓸 수 있다. 파일의 `url`(직접 주소)에는 경로가 들어 있지 않으므로,
|
|
12630
|
+
* 한 폴더의 파일들을 상대 경로로 읽으려면 이 주소를 기준으로 쓴다.
|
|
12631
|
+
*
|
|
12632
|
+
* 대상은 주소만 알면 읽히는 스토리지(`server_write`, `public_read`, `shared`)다. `private` 스토리지는 404 이고
|
|
12633
|
+
* 응답의 `url`(서명 주소)을 써야 한다.
|
|
12634
|
+
*
|
|
12635
|
+
* @example
|
|
12636
|
+
* ```typescript
|
|
12637
|
+
* const base = cb.storage.getPathUrl(storageId, `/l2d/rigs/${rigId}/`)
|
|
12638
|
+
* const meta = await (await fetch(base + 'meta.json')).json()
|
|
12639
|
+
* img.src = base + 'base.webp'
|
|
12640
|
+
* ```
|
|
12641
|
+
*/
|
|
12642
|
+
getPathUrl(storageId, path) {
|
|
12643
|
+
const encoded = path.split("/").filter(
|
|
12644
|
+
(segment, index, all) => segment !== "" || index === all.length - 1
|
|
12645
|
+
).map((segment) => encodeURIComponent(segment)).join("/");
|
|
12646
|
+
return `${this.http.getBaseUrl()}/v1/file-path/${storageId}/${encoded}`;
|
|
12647
|
+
}
|
|
12566
12648
|
/**
|
|
12567
12649
|
* 이미지 파일 여부 확인
|
|
12568
12650
|
*/
|
|
@@ -13550,8 +13632,8 @@ var SupportAPI = class {
|
|
|
13550
13632
|
}
|
|
13551
13633
|
};
|
|
13552
13634
|
function detectSdkVersion() {
|
|
13553
|
-
if ("6.
|
|
13554
|
-
return "6.
|
|
13635
|
+
if ("6.16.0") {
|
|
13636
|
+
return "6.16.0";
|
|
13555
13637
|
}
|
|
13556
13638
|
return "unknown";
|
|
13557
13639
|
}
|
|
@@ -15138,6 +15220,43 @@ var WebRTCAPI = class _WebRTCAPI {
|
|
|
15138
15220
|
baseUrl: this.webrtcUrl
|
|
15139
15221
|
});
|
|
15140
15222
|
}
|
|
15223
|
+
/**
|
|
15224
|
+
* SFU 방 입장 토큰 발급
|
|
15225
|
+
*
|
|
15226
|
+
* `connect()` 는 미디어가 브라우저끼리 직접(P2P) 오가서 상대에게 IP 가 보이고 참가자가 많은 방을 만들 수 없다.
|
|
15227
|
+
* SFU 는 서버가 미디어를 받아 나눠 주는 방식이라 참가자끼리 IP 를 보지 못하고 수십 명 방이 가능하다.
|
|
15228
|
+
* 이 메서드는 채널 정책(인증, 역할, 정원)을 서버에서 판정한 입장 토큰을 돌려주고, 미디어 연결은 앱이
|
|
15229
|
+
* `livekit-client` 로 직접 한다(SDK 는 `livekit-client` 를 포함하지 않는다).
|
|
15230
|
+
*
|
|
15231
|
+
* @example
|
|
15232
|
+
* ```typescript
|
|
15233
|
+
* import { Room } from 'livekit-client'
|
|
15234
|
+
*
|
|
15235
|
+
* const sfu = await cb.webrtc.getSFUToken({ roomId: 'voice:fanmeeting-1', publish: true })
|
|
15236
|
+
* const room = new Room()
|
|
15237
|
+
* await room.connect(sfu.url, sfu.token)
|
|
15238
|
+
* if (sfu.can_publish) {
|
|
15239
|
+
* await room.localParticipant.setMicrophoneEnabled(true)
|
|
15240
|
+
* }
|
|
15241
|
+
* ```
|
|
15242
|
+
*/
|
|
15243
|
+
async getSFUToken(options) {
|
|
15244
|
+
if (!this.appId) {
|
|
15245
|
+
throw new Error(
|
|
15246
|
+
"WebRTC getSFUToken \uC5D0\uB294 appId \uAC00 \uD544\uC694\uD569\uB2C8\uB2E4. ConnectBase \uCD08\uAE30\uD654 \uC2DC appId \uB97C \uC124\uC815\uD558\uC138\uC694."
|
|
15247
|
+
);
|
|
15248
|
+
}
|
|
15249
|
+
return this.http.post(
|
|
15250
|
+
`/v1/apps/${this.appId}/sfu/token`,
|
|
15251
|
+
{
|
|
15252
|
+
room_id: options.roomId,
|
|
15253
|
+
publish: options.publish,
|
|
15254
|
+
name: options.name,
|
|
15255
|
+
device: options.device
|
|
15256
|
+
},
|
|
15257
|
+
{ baseUrl: this.webrtcUrl }
|
|
15258
|
+
);
|
|
15259
|
+
}
|
|
15141
15260
|
/**
|
|
15142
15261
|
* 앱의 WebRTC 통계 조회
|
|
15143
15262
|
*/
|
package/dist/index.mjs
CHANGED
|
@@ -238,7 +238,7 @@ var RealtimeConnectionError = class extends Error {
|
|
|
238
238
|
this.name = "RealtimeConnectionError";
|
|
239
239
|
this.code = init.code || "unknown";
|
|
240
240
|
this.closeCode = init.closeCode;
|
|
241
|
-
this.retryable = init.retryable ?? isRetryableRealtimeRejection(this.code);
|
|
241
|
+
this.retryable = init.retryable ?? isRetryableRealtimeRejection(this.code, init.closeCode);
|
|
242
242
|
this.details = init.details;
|
|
243
243
|
this.retryAfterMs = init.retryAfterMs;
|
|
244
244
|
}
|
|
@@ -255,8 +255,8 @@ var RealtimeConnectionError = class extends Error {
|
|
|
255
255
|
};
|
|
256
256
|
}
|
|
257
257
|
};
|
|
258
|
-
function isRetryableRealtimeRejection(code) {
|
|
259
|
-
return code === "connection_limit_exceeded" || code === "server_at_capacity" || code === "rate_limited" || code === "auth_unavailable" || code === "internal_error" || code === "invalid_ws_ticket" || code === "ws_ticket_unavailable";
|
|
258
|
+
function isRetryableRealtimeRejection(code, closeCode) {
|
|
259
|
+
return closeCode === 4429 || closeCode === 4500 || closeCode === 4503 || code === "connection_limit_exceeded" || code === "rate_limit_exceeded" || code === "server_at_capacity" || code === "rate_limited" || code === "auth_unavailable" || code === "internal_error" || code === "invalid_ws_ticket" || code === "ws_ticket_unavailable";
|
|
260
260
|
}
|
|
261
261
|
var RealtimeRequestError = class extends Error {
|
|
262
262
|
constructor(init) {
|
|
@@ -3754,6 +3754,20 @@ var REALTIME_CLOSING_NOTICE_CODES = /* @__PURE__ */ new Set([
|
|
|
3754
3754
|
]);
|
|
3755
3755
|
var CLOSE_CODE_TOKEN_EXPIRED = 4401;
|
|
3756
3756
|
var CLOSE_CODE_MEMBER_REVOKED = 4403;
|
|
3757
|
+
function toRealtimeRejection(msg) {
|
|
3758
|
+
if (msg.type !== "error" || msg.request_id || typeof msg.close_code !== "number") {
|
|
3759
|
+
return null;
|
|
3760
|
+
}
|
|
3761
|
+
const details = msg.details;
|
|
3762
|
+
return new RealtimeConnectionError({
|
|
3763
|
+
code: typeof msg.code === "string" ? msg.code : void 0,
|
|
3764
|
+
message: typeof msg.message === "string" ? msg.message : void 0,
|
|
3765
|
+
closeCode: msg.close_code,
|
|
3766
|
+
retryable: typeof msg.retryable === "boolean" ? msg.retryable : void 0,
|
|
3767
|
+
details: details && typeof details === "object" && !Array.isArray(details) ? details : void 0,
|
|
3768
|
+
retryAfterMs: typeof msg.retry_after_ms === "number" && msg.retry_after_ms > 0 ? msg.retry_after_ms : void 0
|
|
3769
|
+
});
|
|
3770
|
+
}
|
|
3757
3771
|
var DatabaseAPI = class {
|
|
3758
3772
|
constructor(http) {
|
|
3759
3773
|
this.realtimeWs = null;
|
|
@@ -4696,18 +4710,13 @@ var DatabaseAPI = class {
|
|
|
4696
4710
|
ticket = await this.realtimeTicket();
|
|
4697
4711
|
} catch (e) {
|
|
4698
4712
|
if (e instanceof WsTicketError) {
|
|
4699
|
-
if (e.status === 429 && this.realtimeOptions) {
|
|
4700
|
-
this.attemptRealtimeReconnect();
|
|
4701
|
-
} else {
|
|
4702
|
-
this.setRealtimeState("disconnected");
|
|
4703
|
-
}
|
|
4704
4713
|
const err = new RealtimeConnectionError({
|
|
4705
4714
|
code: e.code,
|
|
4706
4715
|
message: e.message,
|
|
4707
4716
|
closeCode: 4e3 + e.status,
|
|
4708
4717
|
retryAfterMs: e.retryAfterMs
|
|
4709
4718
|
});
|
|
4710
|
-
this.
|
|
4719
|
+
this.handleRealtimeRejection(err);
|
|
4711
4720
|
throw err;
|
|
4712
4721
|
}
|
|
4713
4722
|
throw e;
|
|
@@ -4741,10 +4750,14 @@ var DatabaseAPI = class {
|
|
|
4741
4750
|
this.realtimeOptions.accessToken
|
|
4742
4751
|
);
|
|
4743
4752
|
if (this.realtimeTokenRefresh) url += "&token_refresh=1";
|
|
4753
|
+
url += "&reject_mode=close";
|
|
4744
4754
|
return new Promise((resolve, reject) => {
|
|
4745
4755
|
try {
|
|
4746
|
-
|
|
4756
|
+
const ws = new WebSocket(url);
|
|
4757
|
+
this.realtimeWs = ws;
|
|
4747
4758
|
let settled = false;
|
|
4759
|
+
let accepted = false;
|
|
4760
|
+
let rejection = null;
|
|
4748
4761
|
const connectTimeout = setTimeout(() => {
|
|
4749
4762
|
if (!settled) {
|
|
4750
4763
|
settled = true;
|
|
@@ -4756,7 +4769,8 @@ var DatabaseAPI = class {
|
|
|
4756
4769
|
reject(new Error("Connection timeout"));
|
|
4757
4770
|
}
|
|
4758
4771
|
}, REALTIME_CONNECT_TIMEOUT_MS);
|
|
4759
|
-
|
|
4772
|
+
const accept = () => {
|
|
4773
|
+
accepted = true;
|
|
4760
4774
|
if (!settled) {
|
|
4761
4775
|
settled = true;
|
|
4762
4776
|
clearTimeout(connectTimeout);
|
|
@@ -4769,15 +4783,28 @@ var DatabaseAPI = class {
|
|
|
4769
4783
|
this.resubscribeAll();
|
|
4770
4784
|
resolve();
|
|
4771
4785
|
};
|
|
4772
|
-
|
|
4786
|
+
ws.onopen = () => {
|
|
4787
|
+
ws.send(JSON.stringify({ type: "ping", timestamp: Date.now() }));
|
|
4788
|
+
};
|
|
4789
|
+
ws.onmessage = (event) => {
|
|
4790
|
+
let msg;
|
|
4773
4791
|
try {
|
|
4774
|
-
|
|
4775
|
-
this.handleRealtimeMessage(msg);
|
|
4792
|
+
msg = JSON.parse(event.data);
|
|
4776
4793
|
} catch {
|
|
4777
4794
|
this.debugLog("Failed to parse realtime message");
|
|
4795
|
+
return;
|
|
4796
|
+
}
|
|
4797
|
+
if (!accepted) {
|
|
4798
|
+
const rejected = toRealtimeRejection(msg);
|
|
4799
|
+
if (rejected) {
|
|
4800
|
+
rejection = rejected;
|
|
4801
|
+
return;
|
|
4802
|
+
}
|
|
4803
|
+
accept();
|
|
4778
4804
|
}
|
|
4805
|
+
this.handleRealtimeMessage(msg);
|
|
4779
4806
|
};
|
|
4780
|
-
|
|
4807
|
+
ws.onclose = (event) => {
|
|
4781
4808
|
this.debugLog(
|
|
4782
4809
|
`Database realtime disconnected (code=${event?.code}, reason=${event?.reason})`
|
|
4783
4810
|
);
|
|
@@ -4786,12 +4813,25 @@ var DatabaseAPI = class {
|
|
|
4786
4813
|
this.clearRealtimeAuthRetry();
|
|
4787
4814
|
const notice = this.realtimeCloseNotice;
|
|
4788
4815
|
this.realtimeCloseNotice = null;
|
|
4816
|
+
const code = event?.code;
|
|
4817
|
+
if (!accepted && code >= 4e3 && code <= 4999) {
|
|
4818
|
+
const err = rejection ?? new RealtimeConnectionError({
|
|
4819
|
+
code: event.reason || void 0,
|
|
4820
|
+
closeCode: code
|
|
4821
|
+
});
|
|
4822
|
+
if (!settled) {
|
|
4823
|
+
settled = true;
|
|
4824
|
+
clearTimeout(connectTimeout);
|
|
4825
|
+
reject(err);
|
|
4826
|
+
}
|
|
4827
|
+
this.handleRealtimeRejection(err);
|
|
4828
|
+
return;
|
|
4829
|
+
}
|
|
4789
4830
|
if (!settled) {
|
|
4790
4831
|
settled = true;
|
|
4791
4832
|
clearTimeout(connectTimeout);
|
|
4792
4833
|
reject(new Error("Connection closed during handshake"));
|
|
4793
4834
|
}
|
|
4794
|
-
const code = event?.code;
|
|
4795
4835
|
if (this.realtimeOptions && this.realtimeState !== "disconnected" && (code === CLOSE_CODE_MEMBER_REVOKED || code === CLOSE_CODE_TOKEN_EXPIRED && !this.http.canRefreshMemberToken(
|
|
4796
4836
|
this.realtimeOptions.accessToken
|
|
4797
4837
|
))) {
|
|
@@ -4809,7 +4849,7 @@ var DatabaseAPI = class {
|
|
|
4809
4849
|
this.attemptRealtimeReconnect();
|
|
4810
4850
|
}
|
|
4811
4851
|
};
|
|
4812
|
-
|
|
4852
|
+
ws.onerror = () => {
|
|
4813
4853
|
this.debugLog("Database realtime error");
|
|
4814
4854
|
this.realtimeOnError?.(new Error("WebSocket connection error"));
|
|
4815
4855
|
};
|
|
@@ -4819,6 +4859,21 @@ var DatabaseAPI = class {
|
|
|
4819
4859
|
}
|
|
4820
4860
|
});
|
|
4821
4861
|
}
|
|
4862
|
+
/**
|
|
4863
|
+
* 서버가 연결을 거부했다(티켓 발급 거부, 또는 reject_mode=close 로 받은 업그레이드 거부). cb.realtime 과 같은 규칙이다 —
|
|
4864
|
+
* 다시 해서 될 거부(`retryable`: 일시 장애, 속도 제한, 동시접속 한도, 만료된 티켓)는 서버가 알려 준 시각(`retryAfterMs`)
|
|
4865
|
+
* 이후로 재연결을 이어 가고, 아니면(자격증명, 멤버 폐기, 할당량) 멈추고 `onRealtimeError` 로 알린다. 재시도를 다 써도
|
|
4866
|
+
* 거부되면 마지막 거부를 알린다.
|
|
4867
|
+
*/
|
|
4868
|
+
handleRealtimeRejection(err) {
|
|
4869
|
+
if (!this.realtimeOptions || this.realtimeState === "disconnected") return;
|
|
4870
|
+
if (err.retryable) {
|
|
4871
|
+
this.attemptRealtimeReconnect(err);
|
|
4872
|
+
return;
|
|
4873
|
+
}
|
|
4874
|
+
this.setRealtimeState("disconnected");
|
|
4875
|
+
this.realtimeOnError?.(err);
|
|
4876
|
+
}
|
|
4822
4877
|
/**
|
|
4823
4878
|
* 서버에 subscribe 메시지를 보내고 서버 subscription_id를 반환하는 Promise
|
|
4824
4879
|
*/
|
|
@@ -4991,21 +5046,25 @@ var DatabaseAPI = class {
|
|
|
4991
5046
|
break;
|
|
4992
5047
|
}
|
|
4993
5048
|
}
|
|
4994
|
-
|
|
5049
|
+
/**
|
|
5050
|
+
* 지수 백오프로 다시 붙는다. cause 는 이번 시도를 끝낸 서버의 거부다 — 알려 준 재시도 시각(`retryAfterMs`)보다 일찍 다시
|
|
5051
|
+
* 하지 않고, 재시도를 다 쓰면 일반 오류 대신 그 거부를 알린다.
|
|
5052
|
+
*/
|
|
5053
|
+
attemptRealtimeReconnect(cause) {
|
|
4995
5054
|
const maxRetries = this.realtimeOptions?.maxRetries ?? 5;
|
|
4996
5055
|
const retryInterval = this.realtimeOptions?.retryInterval ?? 1e3;
|
|
4997
5056
|
if (this.realtimeRetryCount >= maxRetries) {
|
|
4998
5057
|
this.setRealtimeState("disconnected");
|
|
4999
5058
|
this.realtimeOnError?.(
|
|
5000
|
-
new Error("Realtime connection lost. Max retries exceeded.")
|
|
5059
|
+
cause ?? new Error("Realtime connection lost. Max retries exceeded.")
|
|
5001
5060
|
);
|
|
5002
5061
|
return;
|
|
5003
5062
|
}
|
|
5004
5063
|
this.setRealtimeState("connecting");
|
|
5005
5064
|
this.realtimeRetryCount++;
|
|
5006
|
-
const delay = Math.
|
|
5007
|
-
retryInterval * 2 ** (this.realtimeRetryCount - 1),
|
|
5008
|
-
|
|
5065
|
+
const delay = Math.max(
|
|
5066
|
+
Math.min(retryInterval * 2 ** (this.realtimeRetryCount - 1), 3e4),
|
|
5067
|
+
cause?.retryAfterMs ?? 0
|
|
5009
5068
|
);
|
|
5010
5069
|
this.debugLog(
|
|
5011
5070
|
`Reconnecting in ${delay}ms (attempt ${this.realtimeRetryCount}/${maxRetries})`
|
|
@@ -12510,6 +12569,29 @@ var StorageAPI = class {
|
|
|
12510
12569
|
getFileUrl(file) {
|
|
12511
12570
|
return file.url || null;
|
|
12512
12571
|
}
|
|
12572
|
+
/**
|
|
12573
|
+
* 경로 주소 만들기 — 파일을 경로로 읽는 고정 주소 (요청을 보내지 않는다)
|
|
12574
|
+
*
|
|
12575
|
+
* 돌려주는 주소는 그 경로의 파일로 리다이렉트된다. 인증 헤더가 필요 없어 `<img src>`, `fetch()`,
|
|
12576
|
+
* 기준 주소 + 상대 경로 로딩에 쓸 수 있다. 파일의 `url`(직접 주소)에는 경로가 들어 있지 않으므로,
|
|
12577
|
+
* 한 폴더의 파일들을 상대 경로로 읽으려면 이 주소를 기준으로 쓴다.
|
|
12578
|
+
*
|
|
12579
|
+
* 대상은 주소만 알면 읽히는 스토리지(`server_write`, `public_read`, `shared`)다. `private` 스토리지는 404 이고
|
|
12580
|
+
* 응답의 `url`(서명 주소)을 써야 한다.
|
|
12581
|
+
*
|
|
12582
|
+
* @example
|
|
12583
|
+
* ```typescript
|
|
12584
|
+
* const base = cb.storage.getPathUrl(storageId, `/l2d/rigs/${rigId}/`)
|
|
12585
|
+
* const meta = await (await fetch(base + 'meta.json')).json()
|
|
12586
|
+
* img.src = base + 'base.webp'
|
|
12587
|
+
* ```
|
|
12588
|
+
*/
|
|
12589
|
+
getPathUrl(storageId, path) {
|
|
12590
|
+
const encoded = path.split("/").filter(
|
|
12591
|
+
(segment, index, all) => segment !== "" || index === all.length - 1
|
|
12592
|
+
).map((segment) => encodeURIComponent(segment)).join("/");
|
|
12593
|
+
return `${this.http.getBaseUrl()}/v1/file-path/${storageId}/${encoded}`;
|
|
12594
|
+
}
|
|
12513
12595
|
/**
|
|
12514
12596
|
* 이미지 파일 여부 확인
|
|
12515
12597
|
*/
|
|
@@ -13497,8 +13579,8 @@ var SupportAPI = class {
|
|
|
13497
13579
|
}
|
|
13498
13580
|
};
|
|
13499
13581
|
function detectSdkVersion() {
|
|
13500
|
-
if ("6.
|
|
13501
|
-
return "6.
|
|
13582
|
+
if ("6.16.0") {
|
|
13583
|
+
return "6.16.0";
|
|
13502
13584
|
}
|
|
13503
13585
|
return "unknown";
|
|
13504
13586
|
}
|
|
@@ -15085,6 +15167,43 @@ var WebRTCAPI = class _WebRTCAPI {
|
|
|
15085
15167
|
baseUrl: this.webrtcUrl
|
|
15086
15168
|
});
|
|
15087
15169
|
}
|
|
15170
|
+
/**
|
|
15171
|
+
* SFU 방 입장 토큰 발급
|
|
15172
|
+
*
|
|
15173
|
+
* `connect()` 는 미디어가 브라우저끼리 직접(P2P) 오가서 상대에게 IP 가 보이고 참가자가 많은 방을 만들 수 없다.
|
|
15174
|
+
* SFU 는 서버가 미디어를 받아 나눠 주는 방식이라 참가자끼리 IP 를 보지 못하고 수십 명 방이 가능하다.
|
|
15175
|
+
* 이 메서드는 채널 정책(인증, 역할, 정원)을 서버에서 판정한 입장 토큰을 돌려주고, 미디어 연결은 앱이
|
|
15176
|
+
* `livekit-client` 로 직접 한다(SDK 는 `livekit-client` 를 포함하지 않는다).
|
|
15177
|
+
*
|
|
15178
|
+
* @example
|
|
15179
|
+
* ```typescript
|
|
15180
|
+
* import { Room } from 'livekit-client'
|
|
15181
|
+
*
|
|
15182
|
+
* const sfu = await cb.webrtc.getSFUToken({ roomId: 'voice:fanmeeting-1', publish: true })
|
|
15183
|
+
* const room = new Room()
|
|
15184
|
+
* await room.connect(sfu.url, sfu.token)
|
|
15185
|
+
* if (sfu.can_publish) {
|
|
15186
|
+
* await room.localParticipant.setMicrophoneEnabled(true)
|
|
15187
|
+
* }
|
|
15188
|
+
* ```
|
|
15189
|
+
*/
|
|
15190
|
+
async getSFUToken(options) {
|
|
15191
|
+
if (!this.appId) {
|
|
15192
|
+
throw new Error(
|
|
15193
|
+
"WebRTC getSFUToken \uC5D0\uB294 appId \uAC00 \uD544\uC694\uD569\uB2C8\uB2E4. ConnectBase \uCD08\uAE30\uD654 \uC2DC appId \uB97C \uC124\uC815\uD558\uC138\uC694."
|
|
15194
|
+
);
|
|
15195
|
+
}
|
|
15196
|
+
return this.http.post(
|
|
15197
|
+
`/v1/apps/${this.appId}/sfu/token`,
|
|
15198
|
+
{
|
|
15199
|
+
room_id: options.roomId,
|
|
15200
|
+
publish: options.publish,
|
|
15201
|
+
name: options.name,
|
|
15202
|
+
device: options.device
|
|
15203
|
+
},
|
|
15204
|
+
{ baseUrl: this.webrtcUrl }
|
|
15205
|
+
);
|
|
15206
|
+
}
|
|
15088
15207
|
/**
|
|
15089
15208
|
* 앱의 WebRTC 통계 조회
|
|
15090
15209
|
*/
|