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/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.realtimeOnError?.(err);
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
- this.realtimeWs = new WebSocket(url);
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
- this.realtimeWs.onopen = () => {
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
- this.realtimeWs.onmessage = (event) => {
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
- const msg = JSON.parse(event.data);
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
- this.realtimeWs.onclose = (event) => {
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
- this.realtimeWs.onerror = () => {
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
- attemptRealtimeReconnect() {
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.min(
5060
- retryInterval * 2 ** (this.realtimeRetryCount - 1),
5061
- 3e4
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.14.0") {
13554
- return "6.14.0";
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.realtimeOnError?.(err);
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
- this.realtimeWs = new WebSocket(url);
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
- this.realtimeWs.onopen = () => {
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
- this.realtimeWs.onmessage = (event) => {
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
- const msg = JSON.parse(event.data);
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
- this.realtimeWs.onclose = (event) => {
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
- this.realtimeWs.onerror = () => {
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
- attemptRealtimeReconnect() {
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.min(
5007
- retryInterval * 2 ** (this.realtimeRetryCount - 1),
5008
- 3e4
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.14.0") {
13501
- return "6.14.0";
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
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "connectbase-client",
3
- "version": "6.14.0",
3
+ "version": "6.16.0",
4
4
  "description": "Connect Base JavaScript/TypeScript SDK for browser and Node.js",
5
5
  "repository": {
6
6
  "type": "git",