connectbase-client 6.5.0 → 6.6.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/dist/index.js CHANGED
@@ -37,6 +37,7 @@ __export(index_exports, {
37
37
  NativeAPI: () => NativeAPI,
38
38
  OrganizationsAPI: () => OrganizationsAPI,
39
39
  RealtimeConnectionError: () => RealtimeConnectionError,
40
+ RealtimeRequestError: () => RealtimeRequestError,
40
41
  RolesAPI: () => RolesAPI,
41
42
  SessionManager: () => SessionManager,
42
43
  VideoProcessingError: () => VideoProcessingError,
@@ -291,6 +292,7 @@ var RealtimeConnectionError = class extends Error {
291
292
  this.closeCode = init.closeCode;
292
293
  this.retryable = init.retryable ?? isRetryableRealtimeRejection(this.code);
293
294
  this.details = init.details;
295
+ this.retryAfterMs = init.retryAfterMs;
294
296
  }
295
297
  /** `JSON.stringify(err)` 가 `{}` 가 되지 않도록 한다 (ApiError.toJSON 참조). */
296
298
  toJSON() {
@@ -300,13 +302,31 @@ var RealtimeConnectionError = class extends Error {
300
302
  message: this.message,
301
303
  closeCode: this.closeCode,
302
304
  retryable: this.retryable,
303
- details: this.details
305
+ details: this.details,
306
+ retryAfterMs: this.retryAfterMs
304
307
  };
305
308
  }
306
309
  };
307
310
  function isRetryableRealtimeRejection(code) {
308
- return code === "connection_limit_exceeded" || code === "server_at_capacity" || code === "auth_unavailable" || code === "internal_error";
311
+ return code === "connection_limit_exceeded" || code === "server_at_capacity" || code === "rate_limited" || code === "auth_unavailable" || code === "internal_error";
309
312
  }
313
+ var RealtimeRequestError = class extends Error {
314
+ constructor(init) {
315
+ super(init.message || init.code || "Realtime request failed");
316
+ this.name = "RealtimeRequestError";
317
+ this.code = init.code || "unknown";
318
+ this.category = init.category;
319
+ }
320
+ /** `JSON.stringify(err)` 가 `{}` 가 되지 않도록 한다 (ApiError.toJSON 참조). */
321
+ toJSON() {
322
+ return {
323
+ name: this.name,
324
+ code: this.code,
325
+ message: this.message,
326
+ category: this.category
327
+ };
328
+ }
329
+ };
310
330
 
311
331
  // src/api/ai.ts
312
332
  function aiErrorToApiError(err) {
@@ -7958,9 +7978,35 @@ function toConnectionRejection(msg) {
7958
7978
  message: data.message || msg.error,
7959
7979
  closeCode: data.close_code,
7960
7980
  retryable: data.retryable,
7961
- details: data.details
7981
+ details: data.details,
7982
+ retryAfterMs: typeof data.retry_after_ms === "number" && data.retry_after_ms > 0 ? data.retry_after_ms : void 0
7962
7983
  });
7963
7984
  }
7985
+ var HandshakeFailedError = class extends Error {
7986
+ };
7987
+ var ConnectCancelledError = class extends Error {
7988
+ constructor() {
7989
+ super("Realtime connect cancelled by disconnect()");
7990
+ this.name = "AbortError";
7991
+ }
7992
+ };
7993
+ var MAX_HANDSHAKE_RETRIES = 2;
7994
+ function jitter(max) {
7995
+ return Math.floor(Math.random() * Math.max(0, max));
7996
+ }
7997
+ function toSendPolicy(value) {
7998
+ return value === "upstream" || value === "publishers_only" ? value : "open";
7999
+ }
8000
+ function toCategoryInfo(response) {
8001
+ return {
8002
+ category: response.category,
8003
+ persist: response.persist,
8004
+ historyCount: response.history_count,
8005
+ readReceipt: response.read_receipt,
8006
+ isPublisher: response.publisher === true,
8007
+ sendPolicy: toSendPolicy(response.send_policy)
8008
+ };
8009
+ }
7964
8010
  var RealtimeAPI = class {
7965
8011
  constructor(http, socketUrl) {
7966
8012
  this.ws = null;
@@ -7998,6 +8044,18 @@ var RealtimeAPI = class {
7998
8044
  this.pendingRestore = null;
7999
8045
  /** 첫 연결 전에 direct 원본 힌트를 한 번 물었는지 (primeDirectRealtime). */
8000
8046
  this.directRealtimePrimed = false;
8047
+ /** disconnect() 마다 늘린다 — connect() 의 재시도 대기가 그 사이 취소됐는지 알아보는 데 쓴다. */
8048
+ this.connectGeneration = 0;
8049
+ /**
8050
+ * connect() 가 첫 연결을 시도하는 중인지. 이때의 핸드셰이크 실패는 connect() 가 직접 다시 시도하거나
8051
+ * 폴백하므로, onclose 가 백그라운드 재연결 체인(handleDisconnect)을 따로 시작하면 안 된다 — 두 경로가
8052
+ * 동시에 연결을 열게 된다.
8053
+ */
8054
+ this.initialConnecting = false;
8055
+ /** 다음 재연결 대기의 하한(ms) — 서버가 알려 준 retry_after_ms. handleDisconnect 가 한 번 쓰고 비운다. */
8056
+ this.reconnectFloorMs = 0;
8057
+ /** connect() 의 재시도 대기를 끝내는 함수 — disconnect() 가 기다림 없이 바로 취소하게 한다. */
8058
+ this.cancelConnectWait = null;
8001
8059
  this.pendingRequests = /* @__PURE__ */ new Map();
8002
8060
  this.subscriptions = /* @__PURE__ */ new Map();
8003
8061
  this.streamSessions = /* @__PURE__ */ new Map();
@@ -8054,12 +8112,12 @@ var RealtimeAPI = class {
8054
8112
  );
8055
8113
  }
8056
8114
  this.connectPromise = this.primeDirectRealtime().then(
8057
- () => this.doConnect()
8115
+ () => this.connectWithRetry()
8058
8116
  );
8059
8117
  try {
8060
8118
  await this.connectPromise;
8061
8119
  } catch (e) {
8062
- if (e instanceof RealtimeConnectionError) {
8120
+ if (e instanceof RealtimeConnectionError || e instanceof ConnectCancelledError) {
8063
8121
  throw e;
8064
8122
  }
8065
8123
  const hasAuth = !!(this.options.accessToken || this.http.getPublicKey());
@@ -8105,6 +8163,8 @@ var RealtimeAPI = class {
8105
8163
  * 연결 해제
8106
8164
  */
8107
8165
  disconnect() {
8166
+ this.connectGeneration++;
8167
+ this.cancelConnectWait?.();
8108
8168
  this.state = "disconnected";
8109
8169
  this.notifyStateChange();
8110
8170
  if (this.ws) {
@@ -8163,12 +8223,7 @@ var RealtimeAPI = class {
8163
8223
  action: "subscribe",
8164
8224
  request_id: requestId
8165
8225
  });
8166
- const info = {
8167
- category: response.category,
8168
- persist: response.persist,
8169
- historyCount: response.history_count,
8170
- readReceipt: response.read_receipt
8171
- };
8226
+ const info = toCategoryInfo(response);
8172
8227
  const handlers = [];
8173
8228
  this.subscriptions.set(category, { info, handlers });
8174
8229
  const subscription = {
@@ -8224,7 +8279,7 @@ var RealtimeAPI = class {
8224
8279
  await this.sendRequest({
8225
8280
  category,
8226
8281
  action: "send",
8227
- data: { data, broadcast },
8282
+ data: options.to ? { data, broadcast, to: options.to } : { data, broadcast },
8228
8283
  request_id: requestId
8229
8284
  });
8230
8285
  }
@@ -8943,18 +8998,79 @@ var RealtimeAPI = class {
8943
8998
  async primeDirectRealtime() {
8944
8999
  if (this.directRealtimePrimed) return;
8945
9000
  this.directRealtimePrimed = true;
8946
- if (this.http.getDirectRealtimeBaseUrl() || !this.http.getPublicKey()) {
9001
+ if (this.http.getDirectRealtimeBaseUrl()) {
8947
9002
  return;
8948
9003
  }
8949
9004
  try {
8950
- await this.http.get("/v1/public/edge-hints", { timeout: 1500 });
9005
+ await this.http.primeEdgeHints(1500);
8951
9006
  } catch (e) {
8952
9007
  this.log(
8953
9008
  `edge hint lookup failed, using default socket URL: ${e instanceof Error ? e.message : String(e)}`
8954
9009
  );
8955
9010
  }
8956
9011
  }
8957
- async doConnect() {
9012
+ /**
9013
+ * 첫 연결을 연결 제한 시간(`timeout`) 안에서 다시 시도한다 (6.6.0+, platform-issue 01a15e24).
9014
+ *
9015
+ * - 서버가 재시도 시각을 알려 준 거부(`retryAfterMs`, 예: 한 IP 에서 연결이 몰린 `rate_limited`):
9016
+ * 그 시각에 지터를 더해 다시 붙는다. 공연 시작처럼 한꺼번에 들어올 때 앱이 재시도를 짜지 않아도 된다.
9017
+ * - 이유 없는 핸드셰이크 실패: 두 번까지 백오프 후 다시 한다. 네트워크가 WebSocket 을 실제로 막으면
9018
+ * 매번 실패하고, 앞단의 일시 거부는 다시 하면 붙는다 — 한 번 실패로 SSE 폴백하던 것이 연결 속도
9019
+ * 제한을 "네트워크 차단" 으로 오진한 원인이었다.
9020
+ * - 그 밖의 거부(자격증명, 동시접속 한도 등)와 시간 초과는 바로 던진다.
9021
+ */
9022
+ async connectWithRetry() {
9023
+ const generation = this.connectGeneration;
9024
+ const deadline = Date.now() + this.options.timeout;
9025
+ let handshakeFailures = 0;
9026
+ this.initialConnecting = true;
9027
+ try {
9028
+ for (let attempt = 1; ; attempt++) {
9029
+ try {
9030
+ await this.doConnect(Math.max(deadline - Date.now(), 1));
9031
+ return;
9032
+ } catch (e) {
9033
+ let wait = null;
9034
+ if (e instanceof RealtimeConnectionError) {
9035
+ if (e.retryable && e.retryAfterMs !== void 0) {
9036
+ wait = e.retryAfterMs + jitter(Math.min(100 * 2 ** attempt, 3e3));
9037
+ } else if (e.code === "rate_limited") {
9038
+ const base = Math.min(500 * 2 ** (attempt - 1), 8e3);
9039
+ wait = base / 2 + jitter(base / 2);
9040
+ }
9041
+ } else if (e instanceof HandshakeFailedError && handshakeFailures < MAX_HANDSHAKE_RETRIES) {
9042
+ handshakeFailures++;
9043
+ const base = 500 * 2 ** (handshakeFailures - 1);
9044
+ wait = base / 2 + jitter(base / 2);
9045
+ }
9046
+ if (wait === null || Date.now() + wait >= deadline) {
9047
+ if (this.state === "connecting") {
9048
+ this.state = "disconnected";
9049
+ this.notifyStateChange();
9050
+ }
9051
+ throw e;
9052
+ }
9053
+ this.log(
9054
+ `Connect attempt ${attempt} failed (${e instanceof Error ? e.message : String(e)}); retrying in ${wait}ms`
9055
+ );
9056
+ await new Promise((resolve) => {
9057
+ const timer = setTimeout(resolve, wait);
9058
+ this.cancelConnectWait = () => {
9059
+ clearTimeout(timer);
9060
+ resolve();
9061
+ };
9062
+ });
9063
+ this.cancelConnectWait = null;
9064
+ if (generation !== this.connectGeneration) {
9065
+ throw new ConnectCancelledError();
9066
+ }
9067
+ }
9068
+ }
9069
+ } finally {
9070
+ this.initialConnecting = false;
9071
+ }
9072
+ }
9073
+ async doConnect(timeoutMs = this.options.timeout) {
8958
9074
  return new Promise((resolve, reject) => {
8959
9075
  this.state = "connecting";
8960
9076
  this.notifyStateChange();
@@ -9000,7 +9116,7 @@ var RealtimeAPI = class {
9000
9116
  new Error(`Connection timeout after ${this.options.timeout}ms`)
9001
9117
  );
9002
9118
  }
9003
- }, this.options.timeout);
9119
+ }, timeoutMs);
9004
9120
  try {
9005
9121
  this.log(`Connecting to ${wsUrl}`);
9006
9122
  this.ws = new WebSocket(url);
@@ -9063,11 +9179,16 @@ var RealtimeAPI = class {
9063
9179
  clearTimeout(timeoutId);
9064
9180
  if (usingDirect) this.http.disableDirectRealtime();
9065
9181
  reject(
9066
- new Error(
9182
+ new HandshakeFailedError(
9067
9183
  `Connection closed: ${event.reason || "unknown reason"}`
9068
9184
  )
9069
9185
  );
9070
9186
  }
9187
+ if (this.initialConnecting && this.state === "connecting") {
9188
+ this.state = "disconnected";
9189
+ this.ws = null;
9190
+ return;
9191
+ }
9071
9192
  if (this.state === "connected" || this.state === "connecting") {
9072
9193
  this.handleDisconnect();
9073
9194
  }
@@ -9075,12 +9196,14 @@ var RealtimeAPI = class {
9075
9196
  this.ws.onerror = (error) => {
9076
9197
  this.log("WebSocket error occurred");
9077
9198
  this.logError("WebSocket error", error);
9078
- this.notifyError(new Error("WebSocket connection error"));
9199
+ if (!this.initialConnecting) {
9200
+ this.notifyError(new Error("WebSocket connection error"));
9201
+ }
9079
9202
  if (!settled && this.state === "connecting") {
9080
9203
  settled = true;
9081
9204
  clearTimeout(timeoutId);
9082
9205
  if (usingDirect) this.http.disableDirectRealtime();
9083
- reject(new Error("Failed to connect"));
9206
+ reject(new HandshakeFailedError("Failed to connect"));
9084
9207
  }
9085
9208
  };
9086
9209
  } catch (e) {
@@ -9144,7 +9267,8 @@ var RealtimeAPI = class {
9144
9267
  category: data.category,
9145
9268
  from: data.from,
9146
9269
  data: data.data,
9147
- sentAt: data.sent_at
9270
+ sentAt: data.sent_at,
9271
+ fromPublisher: data.from_publisher === true
9148
9272
  };
9149
9273
  sub.handlers.forEach((h) => h(message));
9150
9274
  }
@@ -9155,7 +9279,14 @@ var RealtimeAPI = class {
9155
9279
  const pending = this.pendingRequests.get(msg.request_id);
9156
9280
  if (pending) {
9157
9281
  clearTimeout(pending.timeout);
9158
- pending.reject(new Error(msg.error || "Unknown error"));
9282
+ const errData = msg.data;
9283
+ pending.reject(
9284
+ new RealtimeRequestError({
9285
+ code: errData?.code,
9286
+ message: msg.error || "Unknown error",
9287
+ category: msg.category
9288
+ })
9289
+ );
9159
9290
  this.pendingRequests.delete(msg.request_id);
9160
9291
  }
9161
9292
  } else {
@@ -9306,8 +9437,10 @@ var RealtimeAPI = class {
9306
9437
  this.streamSessions.clear();
9307
9438
  if (!this.pendingRestore) {
9308
9439
  const subscriptions = /* @__PURE__ */ new Map();
9440
+ const infos = /* @__PURE__ */ new Map();
9309
9441
  for (const [category, sub] of this.subscriptions) {
9310
9442
  subscriptions.set(category, [...sub.handlers]);
9443
+ infos.set(category, sub.info);
9311
9444
  }
9312
9445
  const presence = /* @__PURE__ */ new Map();
9313
9446
  for (const [userId, handlers] of this.presenceSubscriptions) {
@@ -9317,7 +9450,7 @@ var RealtimeAPI = class {
9317
9450
  for (const [roomId, handlers] of this.typingHandlers) {
9318
9451
  typing.set(roomId, [...handlers]);
9319
9452
  }
9320
- this.pendingRestore = { subscriptions, presence, typing };
9453
+ this.pendingRestore = { subscriptions, infos, presence, typing };
9321
9454
  }
9322
9455
  this.subscriptions.clear();
9323
9456
  this.presenceSubscriptions.clear();
@@ -9326,10 +9459,15 @@ var RealtimeAPI = class {
9326
9459
  this.state = "reconnecting";
9327
9460
  this.notifyStateChange();
9328
9461
  this.retryCount++;
9329
- const delay = Math.min(
9462
+ const base = Math.min(
9330
9463
  this.options.retryInterval * 2 ** (this.retryCount - 1),
9331
9464
  3e4
9332
9465
  );
9466
+ const delay = Math.max(
9467
+ Math.ceil(base / 2) + jitter(Math.floor(base / 2)),
9468
+ this.reconnectFloorMs
9469
+ );
9470
+ this.reconnectFloorMs = 0;
9333
9471
  setTimeout(async () => {
9334
9472
  if (this.sseFallbackActive) return;
9335
9473
  if (this.state !== "reconnecting") return;
@@ -9342,13 +9480,15 @@ var RealtimeAPI = class {
9342
9480
  await this.restoreSubscriptions(
9343
9481
  snapshot.subscriptions,
9344
9482
  snapshot.presence,
9345
- snapshot.typing
9483
+ snapshot.typing,
9484
+ snapshot.infos
9346
9485
  );
9347
9486
  }
9348
9487
  } catch (e) {
9349
9488
  this.logError("Reconnect failed", e);
9350
9489
  if (e instanceof RealtimeConnectionError) {
9351
9490
  if (e.retryable && this.retryCount < this.options.maxRetries) {
9491
+ this.reconnectFloorMs = e.retryAfterMs ?? 0;
9352
9492
  this.handleDisconnect();
9353
9493
  } else {
9354
9494
  this.failRealtime(e);
@@ -9388,7 +9528,7 @@ var RealtimeAPI = class {
9388
9528
  * 이전 구독 상태를 복구합니다.
9389
9529
  * 재연결 성공 후 호출됩니다.
9390
9530
  */
9391
- async restoreSubscriptions(previousSubscriptions, previousPresenceSubscriptions, previousTypingHandlers) {
9531
+ async restoreSubscriptions(previousSubscriptions, previousPresenceSubscriptions, previousTypingHandlers, previousInfos) {
9392
9532
  for (const [category, handlers] of previousSubscriptions) {
9393
9533
  try {
9394
9534
  this.log(`Restoring subscription: ${category}`);
@@ -9398,12 +9538,9 @@ var RealtimeAPI = class {
9398
9538
  action: "subscribe",
9399
9539
  request_id: requestId
9400
9540
  });
9401
- const info = {
9402
- category: response.category,
9403
- persist: response.persist,
9404
- historyCount: response.history_count,
9405
- readReceipt: response.read_receipt
9406
- };
9541
+ const fresh = toCategoryInfo(response);
9542
+ const info = previousInfos?.get(category) ?? fresh;
9543
+ Object.assign(info, fresh);
9407
9544
  this.subscriptions.set(category, { info, handlers });
9408
9545
  this.log(`Restored subscription: ${category}`);
9409
9546
  } catch (e) {
@@ -9456,7 +9593,7 @@ var RealtimeAPI = class {
9456
9593
  assertBidirectional(feature) {
9457
9594
  if (this.activeTransport === "sse") {
9458
9595
  throw new Error(
9459
- `realtime.${feature}() requires a WebSocket connection, which is blocked on this network. Only AI streaming (realtime.stream) works in SSE fallback mode (cb.realtime.transport === 'sse').`
9596
+ `realtime.${feature}() requires a WebSocket connection, which could not be established (the network may block WebSocket, or the connection was repeatedly refused). Only AI streaming (realtime.stream) works in SSE fallback mode (cb.realtime.transport === 'sse').`
9460
9597
  );
9461
9598
  }
9462
9599
  }
@@ -9610,6 +9747,28 @@ function createTimeoutController(options = {}) {
9610
9747
  }
9611
9748
  };
9612
9749
  }
9750
+ function withAbort(promise, signal) {
9751
+ if (!signal) return promise;
9752
+ const reason = () => signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
9753
+ if (signal.aborted) {
9754
+ promise.catch(() => void 0);
9755
+ return Promise.reject(reason());
9756
+ }
9757
+ return new Promise((resolve, reject) => {
9758
+ const onAbort = () => reject(reason());
9759
+ signal.addEventListener("abort", onAbort, { once: true });
9760
+ promise.then(
9761
+ (value) => {
9762
+ signal.removeEventListener("abort", onAbort);
9763
+ resolve(value);
9764
+ },
9765
+ (error) => {
9766
+ signal.removeEventListener("abort", onAbort);
9767
+ reject(error);
9768
+ }
9769
+ );
9770
+ });
9771
+ }
9613
9772
 
9614
9773
  // src/core/url-validation.ts
9615
9774
  var LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1"]);
@@ -12977,14 +13136,14 @@ var HttpClient = class {
12977
13136
  if (credential) {
12978
13137
  headers["X-Public-Key"] = credential;
12979
13138
  }
12980
- const response = await fetch(
12981
- `${this.config.baseUrl}/v1/auth/re-issue`,
12982
- {
13139
+ const response = await withAbort(
13140
+ fetch(`${this.config.baseUrl}/v1/auth/re-issue`, {
12983
13141
  method: "POST",
12984
13142
  headers,
12985
13143
  credentials: "include",
12986
13144
  signal
12987
- }
13145
+ }),
13146
+ signal
12988
13147
  );
12989
13148
  if (!response.ok) {
12990
13149
  const status = response.status;
@@ -13321,40 +13480,73 @@ var HttpClient = class {
13321
13480
  */
13322
13481
  learnDirectOrigin(response) {
13323
13482
  if (!response.headers) return;
13483
+ this.adoptDirectBases(
13484
+ response.headers.get("X-CB-Direct-Base"),
13485
+ response.headers.get("X-CB-Realtime-Direct-Base")
13486
+ );
13487
+ }
13488
+ /**
13489
+ * 광고된 direct 원본을 검증해 받아들인다. 응답 헤더(learnDirectOrigin)와 edge-hints 본문
13490
+ * (primeEdgeHints)이 같은 검증을 거친다.
13491
+ */
13492
+ adoptDirectBases(advertised, advertisedRealtime) {
13324
13493
  const tail = (h) => h.split(".").slice(-2).join(".");
13325
13494
  let base = null;
13326
13495
  try {
13327
13496
  base = new URL(this.config.baseUrl);
13328
13497
  } catch {
13329
13498
  }
13330
- if (base && !this.directDisabled && !this.directBaseUrl) {
13331
- const advertised = response.headers.get("X-CB-Direct-Base");
13332
- if (advertised) {
13333
- try {
13334
- const next = new URL(advertised);
13335
- if (next.protocol === "https:" && tail(next.hostname) === tail(base.hostname)) {
13336
- this.directBaseUrl = next.origin;
13337
- }
13338
- } catch {
13499
+ if (base && !this.directDisabled && !this.directBaseUrl && typeof advertised === "string" && advertised) {
13500
+ try {
13501
+ const next = new URL(advertised);
13502
+ if (next.protocol === "https:" && tail(next.hostname) === tail(base.hostname)) {
13503
+ this.directBaseUrl = next.origin;
13339
13504
  }
13505
+ } catch {
13340
13506
  }
13341
13507
  }
13342
- if (!this.directRealtimeDisabled && !this.directRealtimeBaseUrl) {
13508
+ if (!this.directRealtimeDisabled && !this.directRealtimeBaseUrl && typeof advertisedRealtime === "string" && advertisedRealtime) {
13343
13509
  const expectedTail = base ? tail(base.hostname) : "connectbase.world";
13344
- const advertisedRealtime = response.headers.get(
13345
- "X-CB-Realtime-Direct-Base"
13346
- );
13347
- if (advertisedRealtime) {
13348
- try {
13349
- const next = new URL(advertisedRealtime);
13350
- if (next.protocol === "wss:" && tail(next.hostname) === expectedTail) {
13351
- this.directRealtimeBaseUrl = next.origin;
13352
- }
13353
- } catch {
13510
+ try {
13511
+ const next = new URL(advertisedRealtime);
13512
+ if (next.protocol === "wss:" && tail(next.hostname) === expectedTail) {
13513
+ this.directRealtimeBaseUrl = next.origin;
13354
13514
  }
13515
+ } catch {
13355
13516
  }
13356
13517
  }
13357
13518
  }
13519
+ /**
13520
+ * realtime 첫 연결 전에 저지연 direct 원본 힌트를 묻는다 (`GET /v1/public/edge-hints`, 6.6.0+).
13521
+ *
13522
+ * **커스텀 헤더 없는 단순 GET** 이다. `X-Public-Key` 같은 헤더가 붙으면 브라우저가 CORS 사전 요청을
13523
+ * 먼저 보내 Cloudflare 를 두 번 왕복하고, 앱의 "로컬 개발 origin 허용" 이 꺼진 localhost 페이지에서는
13524
+ * 키 기반 opt-in 에 막혀 브라우저만 direct 를 못 썼다(Node 는 썼다 — platform-issue 01a15e24).
13525
+ * 서버는 이 경로를 키 없이 열어 두고 같은 값을 본문으로도 준다. 본문을 읽으므로 CORS
13526
+ * Expose-Headers 에도 기대지 않는다. 실패는 조용히 무시한다 — direct 는 최적화일 뿐이다.
13527
+ */
13528
+ async primeEdgeHints(timeoutMs) {
13529
+ if (this.directRealtimeBaseUrl || this.directRealtimeDisabled) return;
13530
+ const { signal, cleanup } = createTimeoutController({ timeout: timeoutMs });
13531
+ try {
13532
+ const response = await withAbort(
13533
+ fetch(`${this.config.baseUrl}/v1/public/edge-hints`, {
13534
+ method: "GET",
13535
+ credentials: "omit",
13536
+ signal
13537
+ }),
13538
+ signal
13539
+ );
13540
+ if (!response.ok) {
13541
+ throw new Error(`edge-hints HTTP ${response.status}`);
13542
+ }
13543
+ this.learnDirectOrigin(response);
13544
+ const body = await withAbort(response.json(), signal);
13545
+ this.adoptDirectBases(body?.direct_base, body?.realtime_direct_base);
13546
+ } finally {
13547
+ cleanup();
13548
+ }
13549
+ }
13358
13550
  /**
13359
13551
  * SDK 인증 헤더를 **주입하지 않는** raw fetch. 저지연 원본 선택과 폴백만 얹는다.
13360
13552
  *
@@ -13386,11 +13578,14 @@ var HttpClient = class {
13386
13578
  });
13387
13579
  const origin = this.resolveOrigin(url, config?.baseUrl);
13388
13580
  try {
13389
- const response = await fetch(`${origin}${url}`, {
13390
- ...init,
13391
- credentials: fetchCredentialsForPath(url),
13581
+ const response = await withAbort(
13582
+ fetch(`${origin}${url}`, {
13583
+ ...init,
13584
+ credentials: fetchCredentialsForPath(url),
13585
+ signal
13586
+ }),
13392
13587
  signal
13393
- });
13588
+ );
13394
13589
  if (!config?.baseUrl) {
13395
13590
  this.learnDirectOrigin(response);
13396
13591
  }
@@ -13398,11 +13593,14 @@ var HttpClient = class {
13398
13593
  } catch (err) {
13399
13594
  if (!config?.baseUrl && origin !== this.config.baseUrl && !isAbortError(err)) {
13400
13595
  this.directDisabled = true;
13401
- const response = await fetch(`${this.config.baseUrl}${url}`, {
13402
- ...init,
13403
- credentials: fetchCredentialsForPath(url),
13596
+ const response = await withAbort(
13597
+ fetch(`${this.config.baseUrl}${url}`, {
13598
+ ...init,
13599
+ credentials: fetchCredentialsForPath(url),
13600
+ signal
13601
+ }),
13404
13602
  signal
13405
- });
13603
+ );
13406
13604
  return { response, ok: response.ok, status: response.status };
13407
13605
  }
13408
13606
  throw err;
@@ -14362,6 +14560,7 @@ var index_default = ConnectBase;
14362
14560
  NativeAPI,
14363
14561
  OrganizationsAPI,
14364
14562
  RealtimeConnectionError,
14563
+ RealtimeRequestError,
14365
14564
  RolesAPI,
14366
14565
  SessionManager,
14367
14566
  VideoProcessingError,