connectbase-client 6.5.0 → 6.6.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.mjs CHANGED
@@ -240,6 +240,7 @@ var RealtimeConnectionError = class extends Error {
240
240
  this.closeCode = init.closeCode;
241
241
  this.retryable = init.retryable ?? isRetryableRealtimeRejection(this.code);
242
242
  this.details = init.details;
243
+ this.retryAfterMs = init.retryAfterMs;
243
244
  }
244
245
  /** `JSON.stringify(err)` 가 `{}` 가 되지 않도록 한다 (ApiError.toJSON 참조). */
245
246
  toJSON() {
@@ -249,13 +250,31 @@ var RealtimeConnectionError = class extends Error {
249
250
  message: this.message,
250
251
  closeCode: this.closeCode,
251
252
  retryable: this.retryable,
252
- details: this.details
253
+ details: this.details,
254
+ retryAfterMs: this.retryAfterMs
253
255
  };
254
256
  }
255
257
  };
256
258
  function isRetryableRealtimeRejection(code) {
257
- return code === "connection_limit_exceeded" || code === "server_at_capacity" || code === "auth_unavailable" || code === "internal_error";
259
+ return code === "connection_limit_exceeded" || code === "server_at_capacity" || code === "rate_limited" || code === "auth_unavailable" || code === "internal_error";
258
260
  }
261
+ var RealtimeRequestError = class extends Error {
262
+ constructor(init) {
263
+ super(init.message || init.code || "Realtime request failed");
264
+ this.name = "RealtimeRequestError";
265
+ this.code = init.code || "unknown";
266
+ this.category = init.category;
267
+ }
268
+ /** `JSON.stringify(err)` 가 `{}` 가 되지 않도록 한다 (ApiError.toJSON 참조). */
269
+ toJSON() {
270
+ return {
271
+ name: this.name,
272
+ code: this.code,
273
+ message: this.message,
274
+ category: this.category
275
+ };
276
+ }
277
+ };
259
278
 
260
279
  // src/api/ai.ts
261
280
  function aiErrorToApiError(err) {
@@ -7907,9 +7926,35 @@ function toConnectionRejection(msg) {
7907
7926
  message: data.message || msg.error,
7908
7927
  closeCode: data.close_code,
7909
7928
  retryable: data.retryable,
7910
- details: data.details
7929
+ details: data.details,
7930
+ retryAfterMs: typeof data.retry_after_ms === "number" && data.retry_after_ms > 0 ? data.retry_after_ms : void 0
7911
7931
  });
7912
7932
  }
7933
+ var HandshakeFailedError = class extends Error {
7934
+ };
7935
+ var ConnectCancelledError = class extends Error {
7936
+ constructor() {
7937
+ super("Realtime connect cancelled by disconnect()");
7938
+ this.name = "AbortError";
7939
+ }
7940
+ };
7941
+ var MAX_HANDSHAKE_RETRIES = 2;
7942
+ function jitter(max) {
7943
+ return Math.floor(Math.random() * Math.max(0, max));
7944
+ }
7945
+ function toSendPolicy(value) {
7946
+ return value === "upstream" || value === "publishers_only" ? value : "open";
7947
+ }
7948
+ function toCategoryInfo(response) {
7949
+ return {
7950
+ category: response.category,
7951
+ persist: response.persist,
7952
+ historyCount: response.history_count,
7953
+ readReceipt: response.read_receipt,
7954
+ isPublisher: response.publisher === true,
7955
+ sendPolicy: toSendPolicy(response.send_policy)
7956
+ };
7957
+ }
7913
7958
  var RealtimeAPI = class {
7914
7959
  constructor(http, socketUrl) {
7915
7960
  this.ws = null;
@@ -7947,6 +7992,18 @@ var RealtimeAPI = class {
7947
7992
  this.pendingRestore = null;
7948
7993
  /** 첫 연결 전에 direct 원본 힌트를 한 번 물었는지 (primeDirectRealtime). */
7949
7994
  this.directRealtimePrimed = false;
7995
+ /** disconnect() 마다 늘린다 — connect() 의 재시도 대기가 그 사이 취소됐는지 알아보는 데 쓴다. */
7996
+ this.connectGeneration = 0;
7997
+ /**
7998
+ * connect() 가 첫 연결을 시도하는 중인지. 이때의 핸드셰이크 실패는 connect() 가 직접 다시 시도하거나
7999
+ * 폴백하므로, onclose 가 백그라운드 재연결 체인(handleDisconnect)을 따로 시작하면 안 된다 — 두 경로가
8000
+ * 동시에 연결을 열게 된다.
8001
+ */
8002
+ this.initialConnecting = false;
8003
+ /** 다음 재연결 대기의 하한(ms) — 서버가 알려 준 retry_after_ms. handleDisconnect 가 한 번 쓰고 비운다. */
8004
+ this.reconnectFloorMs = 0;
8005
+ /** connect() 의 재시도 대기를 끝내는 함수 — disconnect() 가 기다림 없이 바로 취소하게 한다. */
8006
+ this.cancelConnectWait = null;
7950
8007
  this.pendingRequests = /* @__PURE__ */ new Map();
7951
8008
  this.subscriptions = /* @__PURE__ */ new Map();
7952
8009
  this.streamSessions = /* @__PURE__ */ new Map();
@@ -8003,12 +8060,12 @@ var RealtimeAPI = class {
8003
8060
  );
8004
8061
  }
8005
8062
  this.connectPromise = this.primeDirectRealtime().then(
8006
- () => this.doConnect()
8063
+ () => this.connectWithRetry()
8007
8064
  );
8008
8065
  try {
8009
8066
  await this.connectPromise;
8010
8067
  } catch (e) {
8011
- if (e instanceof RealtimeConnectionError) {
8068
+ if (e instanceof RealtimeConnectionError || e instanceof ConnectCancelledError) {
8012
8069
  throw e;
8013
8070
  }
8014
8071
  const hasAuth = !!(this.options.accessToken || this.http.getPublicKey());
@@ -8054,6 +8111,8 @@ var RealtimeAPI = class {
8054
8111
  * 연결 해제
8055
8112
  */
8056
8113
  disconnect() {
8114
+ this.connectGeneration++;
8115
+ this.cancelConnectWait?.();
8057
8116
  this.state = "disconnected";
8058
8117
  this.notifyStateChange();
8059
8118
  if (this.ws) {
@@ -8112,12 +8171,7 @@ var RealtimeAPI = class {
8112
8171
  action: "subscribe",
8113
8172
  request_id: requestId
8114
8173
  });
8115
- const info = {
8116
- category: response.category,
8117
- persist: response.persist,
8118
- historyCount: response.history_count,
8119
- readReceipt: response.read_receipt
8120
- };
8174
+ const info = toCategoryInfo(response);
8121
8175
  const handlers = [];
8122
8176
  this.subscriptions.set(category, { info, handlers });
8123
8177
  const subscription = {
@@ -8173,7 +8227,7 @@ var RealtimeAPI = class {
8173
8227
  await this.sendRequest({
8174
8228
  category,
8175
8229
  action: "send",
8176
- data: { data, broadcast },
8230
+ data: options.to ? { data, broadcast, to: options.to } : { data, broadcast },
8177
8231
  request_id: requestId
8178
8232
  });
8179
8233
  }
@@ -8892,18 +8946,79 @@ var RealtimeAPI = class {
8892
8946
  async primeDirectRealtime() {
8893
8947
  if (this.directRealtimePrimed) return;
8894
8948
  this.directRealtimePrimed = true;
8895
- if (this.http.getDirectRealtimeBaseUrl() || !this.http.getPublicKey()) {
8949
+ if (this.http.getDirectRealtimeBaseUrl()) {
8896
8950
  return;
8897
8951
  }
8898
8952
  try {
8899
- await this.http.get("/v1/public/edge-hints", { timeout: 1500 });
8953
+ await this.http.primeEdgeHints(1500);
8900
8954
  } catch (e) {
8901
8955
  this.log(
8902
8956
  `edge hint lookup failed, using default socket URL: ${e instanceof Error ? e.message : String(e)}`
8903
8957
  );
8904
8958
  }
8905
8959
  }
8906
- async doConnect() {
8960
+ /**
8961
+ * 첫 연결을 연결 제한 시간(`timeout`) 안에서 다시 시도한다 (6.6.0+, platform-issue 01a15e24).
8962
+ *
8963
+ * - 서버가 재시도 시각을 알려 준 거부(`retryAfterMs`, 예: 한 IP 에서 연결이 몰린 `rate_limited`):
8964
+ * 그 시각에 지터를 더해 다시 붙는다. 공연 시작처럼 한꺼번에 들어올 때 앱이 재시도를 짜지 않아도 된다.
8965
+ * - 이유 없는 핸드셰이크 실패: 두 번까지 백오프 후 다시 한다. 네트워크가 WebSocket 을 실제로 막으면
8966
+ * 매번 실패하고, 앞단의 일시 거부는 다시 하면 붙는다 — 한 번 실패로 SSE 폴백하던 것이 연결 속도
8967
+ * 제한을 "네트워크 차단" 으로 오진한 원인이었다.
8968
+ * - 그 밖의 거부(자격증명, 동시접속 한도 등)와 시간 초과는 바로 던진다.
8969
+ */
8970
+ async connectWithRetry() {
8971
+ const generation = this.connectGeneration;
8972
+ const deadline = Date.now() + this.options.timeout;
8973
+ let handshakeFailures = 0;
8974
+ this.initialConnecting = true;
8975
+ try {
8976
+ for (let attempt = 1; ; attempt++) {
8977
+ try {
8978
+ await this.doConnect(Math.max(deadline - Date.now(), 1));
8979
+ return;
8980
+ } catch (e) {
8981
+ let wait = null;
8982
+ if (e instanceof RealtimeConnectionError) {
8983
+ if (e.retryable && e.retryAfterMs !== void 0) {
8984
+ wait = e.retryAfterMs + jitter(Math.min(100 * 2 ** attempt, 3e3));
8985
+ } else if (e.code === "rate_limited") {
8986
+ const base = Math.min(500 * 2 ** (attempt - 1), 8e3);
8987
+ wait = base / 2 + jitter(base / 2);
8988
+ }
8989
+ } else if (e instanceof HandshakeFailedError && handshakeFailures < MAX_HANDSHAKE_RETRIES) {
8990
+ handshakeFailures++;
8991
+ const base = 500 * 2 ** (handshakeFailures - 1);
8992
+ wait = base / 2 + jitter(base / 2);
8993
+ }
8994
+ if (wait === null || Date.now() + wait >= deadline) {
8995
+ if (this.state === "connecting") {
8996
+ this.state = "disconnected";
8997
+ this.notifyStateChange();
8998
+ }
8999
+ throw e;
9000
+ }
9001
+ this.log(
9002
+ `Connect attempt ${attempt} failed (${e instanceof Error ? e.message : String(e)}); retrying in ${wait}ms`
9003
+ );
9004
+ await new Promise((resolve) => {
9005
+ const timer = setTimeout(resolve, wait);
9006
+ this.cancelConnectWait = () => {
9007
+ clearTimeout(timer);
9008
+ resolve();
9009
+ };
9010
+ });
9011
+ this.cancelConnectWait = null;
9012
+ if (generation !== this.connectGeneration) {
9013
+ throw new ConnectCancelledError();
9014
+ }
9015
+ }
9016
+ }
9017
+ } finally {
9018
+ this.initialConnecting = false;
9019
+ }
9020
+ }
9021
+ async doConnect(timeoutMs = this.options.timeout) {
8907
9022
  return new Promise((resolve, reject) => {
8908
9023
  this.state = "connecting";
8909
9024
  this.notifyStateChange();
@@ -8949,7 +9064,7 @@ var RealtimeAPI = class {
8949
9064
  new Error(`Connection timeout after ${this.options.timeout}ms`)
8950
9065
  );
8951
9066
  }
8952
- }, this.options.timeout);
9067
+ }, timeoutMs);
8953
9068
  try {
8954
9069
  this.log(`Connecting to ${wsUrl}`);
8955
9070
  this.ws = new WebSocket(url);
@@ -9012,11 +9127,16 @@ var RealtimeAPI = class {
9012
9127
  clearTimeout(timeoutId);
9013
9128
  if (usingDirect) this.http.disableDirectRealtime();
9014
9129
  reject(
9015
- new Error(
9130
+ new HandshakeFailedError(
9016
9131
  `Connection closed: ${event.reason || "unknown reason"}`
9017
9132
  )
9018
9133
  );
9019
9134
  }
9135
+ if (this.initialConnecting && this.state === "connecting") {
9136
+ this.state = "disconnected";
9137
+ this.ws = null;
9138
+ return;
9139
+ }
9020
9140
  if (this.state === "connected" || this.state === "connecting") {
9021
9141
  this.handleDisconnect();
9022
9142
  }
@@ -9024,12 +9144,14 @@ var RealtimeAPI = class {
9024
9144
  this.ws.onerror = (error) => {
9025
9145
  this.log("WebSocket error occurred");
9026
9146
  this.logError("WebSocket error", error);
9027
- this.notifyError(new Error("WebSocket connection error"));
9147
+ if (!this.initialConnecting) {
9148
+ this.notifyError(new Error("WebSocket connection error"));
9149
+ }
9028
9150
  if (!settled && this.state === "connecting") {
9029
9151
  settled = true;
9030
9152
  clearTimeout(timeoutId);
9031
9153
  if (usingDirect) this.http.disableDirectRealtime();
9032
- reject(new Error("Failed to connect"));
9154
+ reject(new HandshakeFailedError("Failed to connect"));
9033
9155
  }
9034
9156
  };
9035
9157
  } catch (e) {
@@ -9093,7 +9215,8 @@ var RealtimeAPI = class {
9093
9215
  category: data.category,
9094
9216
  from: data.from,
9095
9217
  data: data.data,
9096
- sentAt: data.sent_at
9218
+ sentAt: data.sent_at,
9219
+ fromPublisher: data.from_publisher === true
9097
9220
  };
9098
9221
  sub.handlers.forEach((h) => h(message));
9099
9222
  }
@@ -9104,7 +9227,14 @@ var RealtimeAPI = class {
9104
9227
  const pending = this.pendingRequests.get(msg.request_id);
9105
9228
  if (pending) {
9106
9229
  clearTimeout(pending.timeout);
9107
- pending.reject(new Error(msg.error || "Unknown error"));
9230
+ const errData = msg.data;
9231
+ pending.reject(
9232
+ new RealtimeRequestError({
9233
+ code: errData?.code,
9234
+ message: msg.error || "Unknown error",
9235
+ category: msg.category
9236
+ })
9237
+ );
9108
9238
  this.pendingRequests.delete(msg.request_id);
9109
9239
  }
9110
9240
  } else {
@@ -9255,8 +9385,10 @@ var RealtimeAPI = class {
9255
9385
  this.streamSessions.clear();
9256
9386
  if (!this.pendingRestore) {
9257
9387
  const subscriptions = /* @__PURE__ */ new Map();
9388
+ const infos = /* @__PURE__ */ new Map();
9258
9389
  for (const [category, sub] of this.subscriptions) {
9259
9390
  subscriptions.set(category, [...sub.handlers]);
9391
+ infos.set(category, sub.info);
9260
9392
  }
9261
9393
  const presence = /* @__PURE__ */ new Map();
9262
9394
  for (const [userId, handlers] of this.presenceSubscriptions) {
@@ -9266,7 +9398,7 @@ var RealtimeAPI = class {
9266
9398
  for (const [roomId, handlers] of this.typingHandlers) {
9267
9399
  typing.set(roomId, [...handlers]);
9268
9400
  }
9269
- this.pendingRestore = { subscriptions, presence, typing };
9401
+ this.pendingRestore = { subscriptions, infos, presence, typing };
9270
9402
  }
9271
9403
  this.subscriptions.clear();
9272
9404
  this.presenceSubscriptions.clear();
@@ -9275,10 +9407,15 @@ var RealtimeAPI = class {
9275
9407
  this.state = "reconnecting";
9276
9408
  this.notifyStateChange();
9277
9409
  this.retryCount++;
9278
- const delay = Math.min(
9410
+ const base = Math.min(
9279
9411
  this.options.retryInterval * 2 ** (this.retryCount - 1),
9280
9412
  3e4
9281
9413
  );
9414
+ const delay = Math.max(
9415
+ Math.ceil(base / 2) + jitter(Math.floor(base / 2)),
9416
+ this.reconnectFloorMs
9417
+ );
9418
+ this.reconnectFloorMs = 0;
9282
9419
  setTimeout(async () => {
9283
9420
  if (this.sseFallbackActive) return;
9284
9421
  if (this.state !== "reconnecting") return;
@@ -9291,13 +9428,15 @@ var RealtimeAPI = class {
9291
9428
  await this.restoreSubscriptions(
9292
9429
  snapshot.subscriptions,
9293
9430
  snapshot.presence,
9294
- snapshot.typing
9431
+ snapshot.typing,
9432
+ snapshot.infos
9295
9433
  );
9296
9434
  }
9297
9435
  } catch (e) {
9298
9436
  this.logError("Reconnect failed", e);
9299
9437
  if (e instanceof RealtimeConnectionError) {
9300
9438
  if (e.retryable && this.retryCount < this.options.maxRetries) {
9439
+ this.reconnectFloorMs = e.retryAfterMs ?? 0;
9301
9440
  this.handleDisconnect();
9302
9441
  } else {
9303
9442
  this.failRealtime(e);
@@ -9337,7 +9476,7 @@ var RealtimeAPI = class {
9337
9476
  * 이전 구독 상태를 복구합니다.
9338
9477
  * 재연결 성공 후 호출됩니다.
9339
9478
  */
9340
- async restoreSubscriptions(previousSubscriptions, previousPresenceSubscriptions, previousTypingHandlers) {
9479
+ async restoreSubscriptions(previousSubscriptions, previousPresenceSubscriptions, previousTypingHandlers, previousInfos) {
9341
9480
  for (const [category, handlers] of previousSubscriptions) {
9342
9481
  try {
9343
9482
  this.log(`Restoring subscription: ${category}`);
@@ -9347,12 +9486,9 @@ var RealtimeAPI = class {
9347
9486
  action: "subscribe",
9348
9487
  request_id: requestId
9349
9488
  });
9350
- const info = {
9351
- category: response.category,
9352
- persist: response.persist,
9353
- historyCount: response.history_count,
9354
- readReceipt: response.read_receipt
9355
- };
9489
+ const fresh = toCategoryInfo(response);
9490
+ const info = previousInfos?.get(category) ?? fresh;
9491
+ Object.assign(info, fresh);
9356
9492
  this.subscriptions.set(category, { info, handlers });
9357
9493
  this.log(`Restored subscription: ${category}`);
9358
9494
  } catch (e) {
@@ -9405,7 +9541,7 @@ var RealtimeAPI = class {
9405
9541
  assertBidirectional(feature) {
9406
9542
  if (this.activeTransport === "sse") {
9407
9543
  throw new Error(
9408
- `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').`
9544
+ `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').`
9409
9545
  );
9410
9546
  }
9411
9547
  }
@@ -9559,6 +9695,28 @@ function createTimeoutController(options = {}) {
9559
9695
  }
9560
9696
  };
9561
9697
  }
9698
+ function withAbort(promise, signal) {
9699
+ if (!signal) return promise;
9700
+ const reason = () => signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
9701
+ if (signal.aborted) {
9702
+ promise.catch(() => void 0);
9703
+ return Promise.reject(reason());
9704
+ }
9705
+ return new Promise((resolve, reject) => {
9706
+ const onAbort = () => reject(reason());
9707
+ signal.addEventListener("abort", onAbort, { once: true });
9708
+ promise.then(
9709
+ (value) => {
9710
+ signal.removeEventListener("abort", onAbort);
9711
+ resolve(value);
9712
+ },
9713
+ (error) => {
9714
+ signal.removeEventListener("abort", onAbort);
9715
+ reject(error);
9716
+ }
9717
+ );
9718
+ });
9719
+ }
9562
9720
 
9563
9721
  // src/core/url-validation.ts
9564
9722
  var LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1"]);
@@ -12926,14 +13084,14 @@ var HttpClient = class {
12926
13084
  if (credential) {
12927
13085
  headers["X-Public-Key"] = credential;
12928
13086
  }
12929
- const response = await fetch(
12930
- `${this.config.baseUrl}/v1/auth/re-issue`,
12931
- {
13087
+ const response = await withAbort(
13088
+ fetch(`${this.config.baseUrl}/v1/auth/re-issue`, {
12932
13089
  method: "POST",
12933
13090
  headers,
12934
13091
  credentials: "include",
12935
13092
  signal
12936
- }
13093
+ }),
13094
+ signal
12937
13095
  );
12938
13096
  if (!response.ok) {
12939
13097
  const status = response.status;
@@ -13270,40 +13428,73 @@ var HttpClient = class {
13270
13428
  */
13271
13429
  learnDirectOrigin(response) {
13272
13430
  if (!response.headers) return;
13431
+ this.adoptDirectBases(
13432
+ response.headers.get("X-CB-Direct-Base"),
13433
+ response.headers.get("X-CB-Realtime-Direct-Base")
13434
+ );
13435
+ }
13436
+ /**
13437
+ * 광고된 direct 원본을 검증해 받아들인다. 응답 헤더(learnDirectOrigin)와 edge-hints 본문
13438
+ * (primeEdgeHints)이 같은 검증을 거친다.
13439
+ */
13440
+ adoptDirectBases(advertised, advertisedRealtime) {
13273
13441
  const tail = (h) => h.split(".").slice(-2).join(".");
13274
13442
  let base = null;
13275
13443
  try {
13276
13444
  base = new URL(this.config.baseUrl);
13277
13445
  } catch {
13278
13446
  }
13279
- if (base && !this.directDisabled && !this.directBaseUrl) {
13280
- const advertised = response.headers.get("X-CB-Direct-Base");
13281
- if (advertised) {
13282
- try {
13283
- const next = new URL(advertised);
13284
- if (next.protocol === "https:" && tail(next.hostname) === tail(base.hostname)) {
13285
- this.directBaseUrl = next.origin;
13286
- }
13287
- } catch {
13447
+ if (base && !this.directDisabled && !this.directBaseUrl && typeof advertised === "string" && advertised) {
13448
+ try {
13449
+ const next = new URL(advertised);
13450
+ if (next.protocol === "https:" && tail(next.hostname) === tail(base.hostname)) {
13451
+ this.directBaseUrl = next.origin;
13288
13452
  }
13453
+ } catch {
13289
13454
  }
13290
13455
  }
13291
- if (!this.directRealtimeDisabled && !this.directRealtimeBaseUrl) {
13456
+ if (!this.directRealtimeDisabled && !this.directRealtimeBaseUrl && typeof advertisedRealtime === "string" && advertisedRealtime) {
13292
13457
  const expectedTail = base ? tail(base.hostname) : "connectbase.world";
13293
- const advertisedRealtime = response.headers.get(
13294
- "X-CB-Realtime-Direct-Base"
13295
- );
13296
- if (advertisedRealtime) {
13297
- try {
13298
- const next = new URL(advertisedRealtime);
13299
- if (next.protocol === "wss:" && tail(next.hostname) === expectedTail) {
13300
- this.directRealtimeBaseUrl = next.origin;
13301
- }
13302
- } catch {
13458
+ try {
13459
+ const next = new URL(advertisedRealtime);
13460
+ if (next.protocol === "wss:" && tail(next.hostname) === expectedTail) {
13461
+ this.directRealtimeBaseUrl = next.origin;
13303
13462
  }
13463
+ } catch {
13304
13464
  }
13305
13465
  }
13306
13466
  }
13467
+ /**
13468
+ * realtime 첫 연결 전에 저지연 direct 원본 힌트를 묻는다 (`GET /v1/public/edge-hints`, 6.6.0+).
13469
+ *
13470
+ * **커스텀 헤더 없는 단순 GET** 이다. `X-Public-Key` 같은 헤더가 붙으면 브라우저가 CORS 사전 요청을
13471
+ * 먼저 보내 Cloudflare 를 두 번 왕복하고, 앱의 "로컬 개발 origin 허용" 이 꺼진 localhost 페이지에서는
13472
+ * 키 기반 opt-in 에 막혀 브라우저만 direct 를 못 썼다(Node 는 썼다 — platform-issue 01a15e24).
13473
+ * 서버는 이 경로를 키 없이 열어 두고 같은 값을 본문으로도 준다. 본문을 읽으므로 CORS
13474
+ * Expose-Headers 에도 기대지 않는다. 실패는 조용히 무시한다 — direct 는 최적화일 뿐이다.
13475
+ */
13476
+ async primeEdgeHints(timeoutMs) {
13477
+ if (this.directRealtimeBaseUrl || this.directRealtimeDisabled) return;
13478
+ const { signal, cleanup } = createTimeoutController({ timeout: timeoutMs });
13479
+ try {
13480
+ const response = await withAbort(
13481
+ fetch(`${this.config.baseUrl}/v1/public/edge-hints`, {
13482
+ method: "GET",
13483
+ credentials: "omit",
13484
+ signal
13485
+ }),
13486
+ signal
13487
+ );
13488
+ if (!response.ok) {
13489
+ throw new Error(`edge-hints HTTP ${response.status}`);
13490
+ }
13491
+ this.learnDirectOrigin(response);
13492
+ const body = await withAbort(response.json(), signal);
13493
+ this.adoptDirectBases(body?.direct_base, body?.realtime_direct_base);
13494
+ } finally {
13495
+ cleanup();
13496
+ }
13497
+ }
13307
13498
  /**
13308
13499
  * SDK 인증 헤더를 **주입하지 않는** raw fetch. 저지연 원본 선택과 폴백만 얹는다.
13309
13500
  *
@@ -13335,11 +13526,14 @@ var HttpClient = class {
13335
13526
  });
13336
13527
  const origin = this.resolveOrigin(url, config?.baseUrl);
13337
13528
  try {
13338
- const response = await fetch(`${origin}${url}`, {
13339
- ...init,
13340
- credentials: fetchCredentialsForPath(url),
13529
+ const response = await withAbort(
13530
+ fetch(`${origin}${url}`, {
13531
+ ...init,
13532
+ credentials: fetchCredentialsForPath(url),
13533
+ signal
13534
+ }),
13341
13535
  signal
13342
- });
13536
+ );
13343
13537
  if (!config?.baseUrl) {
13344
13538
  this.learnDirectOrigin(response);
13345
13539
  }
@@ -13347,11 +13541,14 @@ var HttpClient = class {
13347
13541
  } catch (err) {
13348
13542
  if (!config?.baseUrl && origin !== this.config.baseUrl && !isAbortError(err)) {
13349
13543
  this.directDisabled = true;
13350
- const response = await fetch(`${this.config.baseUrl}${url}`, {
13351
- ...init,
13352
- credentials: fetchCredentialsForPath(url),
13544
+ const response = await withAbort(
13545
+ fetch(`${this.config.baseUrl}${url}`, {
13546
+ ...init,
13547
+ credentials: fetchCredentialsForPath(url),
13548
+ signal
13549
+ }),
13353
13550
  signal
13354
- });
13551
+ );
13355
13552
  return { response, ok: response.ok, status: response.status };
13356
13553
  }
13357
13554
  throw err;
@@ -14310,6 +14507,7 @@ export {
14310
14507
  NativeAPI,
14311
14508
  OrganizationsAPI,
14312
14509
  RealtimeConnectionError,
14510
+ RealtimeRequestError,
14313
14511
  RolesAPI,
14314
14512
  SessionManager,
14315
14513
  VideoProcessingError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "connectbase-client",
3
- "version": "6.5.0",
3
+ "version": "6.6.0",
4
4
  "description": "Connect Base JavaScript/TypeScript SDK for browser and Node.js",
5
5
  "repository": {
6
6
  "type": "git",
@@ -45,6 +45,7 @@
45
45
  "test": "vitest run",
46
46
  "test:types": "tsc --noEmit -p test/tsconfig.json",
47
47
  "release": "pnpm build && npm publish --access public",
48
+ "publish:cdn": "node scripts/publish-cdn.mjs",
48
49
  "lint": "biome lint .",
49
50
  "format": "biome format --write .",
50
51
  "check": "biome check ."