connectbase-client 6.6.1 → 6.8.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
@@ -2074,6 +2074,11 @@ var AuthAPI = class {
2074
2074
  * role: 'editor',
2075
2075
  * custom_data: { level: 5 }
2076
2076
  * })
2077
+ *
2078
+ * // 역할 여러 개 (6.7.0+): role 은 주 역할(RLS auth.role), roles 는 추가 역할 목록을 **통째로 바꾼다**
2079
+ * // ([] 면 비우고, 빼면 그대로). auth.roles, hasRole(), 실시간 publisher_roles 는 role + roles 를 함께 본다.
2080
+ * await cb.auth.adminUpdateMember('member-uuid', { roles: ['performer'] })
2081
+ * // → { role: 'admin', roles: ['performer'], effective_roles: ['admin', 'performer'], ... }
2077
2082
  * ```
2078
2083
  */
2079
2084
  async adminUpdateMember(memberID, fields) {
@@ -7571,7 +7576,7 @@ var PushAPI = class {
7571
7576
  /**
7572
7577
  * 디바이스 등록 해제
7573
7578
  *
7574
- * @param deviceToken 디바이스 토큰
7579
+ * @param deviceToken 디바이스 토큰 (웹 푸시 기기는 등록 응답의 `device_token`, 즉 endpoint URL)
7575
7580
  *
7576
7581
  * @example
7577
7582
  * ```typescript
@@ -7580,13 +7585,18 @@ var PushAPI = class {
7580
7585
  */
7581
7586
  async unregisterDevice(deviceToken) {
7582
7587
  const prefix = this.getPublicPrefix();
7583
- await this.http.delete(`${prefix}/push/devices/${deviceToken}`);
7588
+ await this.http.post(`${prefix}/push/devices/unregister`, {
7589
+ device_token: deviceToken
7590
+ });
7584
7591
  }
7585
7592
  // ============ Topic Subscription ============
7586
7593
  /**
7587
7594
  * 토픽 구독
7588
7595
  *
7589
- * @param deviceToken 디바이스 토큰
7596
+ * 토픽은 미리 있어야 한다(콘솔에서 만든다). 없으면 404 로 거부된다.
7597
+ *
7598
+ * @param deviceToken 디바이스 토큰 — `registerDevice` / `registerWebPush` 응답의 `device_token`
7599
+ * (웹 푸시 기기는 endpoint URL. 6.7.0 이하는 웹 기기를 구독시키지 못했다)
7590
7600
  * @param topicName 토픽 이름
7591
7601
  *
7592
7602
  * @example
@@ -7596,11 +7606,11 @@ var PushAPI = class {
7596
7606
  */
7597
7607
  async subscribeTopic(deviceToken, topicName) {
7598
7608
  const prefix = this.getPublicPrefix();
7599
- const request = { topic_name: topicName };
7600
- await this.http.post(
7601
- `${prefix}/push/devices/${deviceToken}/topics/subscribe`,
7602
- request
7603
- );
7609
+ const request = {
7610
+ device_token: deviceToken,
7611
+ topic_name: topicName
7612
+ };
7613
+ await this.http.post(`${prefix}/push/topics/subscribe`, request);
7604
7614
  }
7605
7615
  /**
7606
7616
  * 토픽 구독 해제
@@ -7615,9 +7625,11 @@ var PushAPI = class {
7615
7625
  */
7616
7626
  async unsubscribeTopic(deviceToken, topicName) {
7617
7627
  const prefix = this.getPublicPrefix();
7618
- await this.http.delete(
7619
- `${prefix}/push/devices/${deviceToken}/topics/${topicName}/unsubscribe`
7620
- );
7628
+ const request = {
7629
+ device_token: deviceToken,
7630
+ topic_name: topicName
7631
+ };
7632
+ await this.http.post(`${prefix}/push/topics/unsubscribe`, request);
7621
7633
  }
7622
7634
  // ============ Web Push ============
7623
7635
  /**
@@ -7694,13 +7706,10 @@ var PushAPI = class {
7694
7706
  /**
7695
7707
  * Web Push 구독 해제
7696
7708
  *
7697
- * @param deviceToken Web Push endpoint URL (등록 시 사용한 endpoint)
7709
+ * @param deviceToken Web Push endpoint URL (등록 응답의 `device_token`)
7698
7710
  */
7699
7711
  async unregisterWebPush(deviceToken) {
7700
- const prefix = this.getPublicPrefix();
7701
- await this.http.delete(
7702
- `${prefix}/push/devices/${encodeURIComponent(deviceToken)}`
7703
- );
7712
+ await this.unregisterDevice(deviceToken);
7704
7713
  }
7705
7714
  // ============ Server-side Send (Functions / cb_sk_ 전용) ============
7706
7715
  /**
@@ -7803,6 +7812,42 @@ var PushAPI = class {
7803
7812
  body
7804
7813
  );
7805
7814
  }
7815
+ /**
7816
+ * 토픽 만들기 — 서버 코드 전용 (6.8.0+).
7817
+ *
7818
+ * 방마다 쓰는 토픽처럼 서버 코드가 토픽을 만들어 두고, 기기는 그 뒤에 `subscribeTopic` 으로 구독한다. 같은 이름의
7819
+ * 토픽이 이미 있으면 오류 없이 그 토픽을 돌려준다(설정은 바꾸지 않는다) — 트리거가 다시 실행돼도 안전하다.
7820
+ *
7821
+ * Public Key(cb_pk_) 단독 인스턴스로는 호출할 수 없다(공개 키로는 토픽을 만들 수 없다). service_role(ctx.cbAdmin)은
7822
+ * 함수의 management_scopes 에 `push:write` 가 있어야 한다. User Secret Key(cb_sk_), 콘솔 JWT 도 된다.
7823
+ *
7824
+ * @param appId 앱 ID
7825
+ * @param name 토픽 이름 (255바이트 이하, 앞뒤 공백은 지운다)
7826
+ * @param options 표시 이름, 설명
7827
+ * @returns 만들었거나 이미 있던 토픽
7828
+ *
7829
+ * @example
7830
+ * ```typescript
7831
+ * // concerts 트리거 함수: 공연이 생기면 그 방의 알림 토픽을 만들어 둔다
7832
+ * await ctx.cbAdmin.push.createTopic(appId, `cc_${row.room}`, { displayName: row.title })
7833
+ * ```
7834
+ */
7835
+ async createTopic(appId, name, options = {}) {
7836
+ if (this.http.hasPublicKey() && !this.http.hasJWT()) {
7837
+ throw new Error(
7838
+ "cb.push.createTopic() \uB294 User Secret Key(cb_sk_), \uCF58\uC194 JWT, \uB610\uB294 service_role(ctx.cbAdmin) \uC778\uC99D\uC774 \uD544\uC694\uD569\uB2C8\uB2E4. Public Key(cb_pk_) \uB2E8\uB3C5 SDK \uC778\uC2A4\uD134\uC2A4\uB85C\uB294 \uD1A0\uD53D\uC744 \uB9CC\uB4E4 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 \u2014 Functions \uD658\uACBD\uC5D0\uC11C \uC0AC\uC6A9\uD558\uC138\uC694."
7839
+ );
7840
+ }
7841
+ if (!name || !name.trim()) {
7842
+ throw new Error("cb.push.createTopic(): name \uC774 \uBE44\uC5B4\uC788\uC2B5\uB2C8\uB2E4.");
7843
+ }
7844
+ return this.http.post(`/v1/apps/${appId}/push/topics`, {
7845
+ name,
7846
+ if_not_exists: true,
7847
+ ...options.displayName !== void 0 ? { display_name: options.displayName } : {},
7848
+ ...options.description !== void 0 ? { description: options.description } : {}
7849
+ });
7850
+ }
7806
7851
  /**
7807
7852
  * 앱의 푸시 통계(도달/오픈율/클릭율, 디바이스/메시지/토픽 수)를 조회한다.
7808
7853
  *
@@ -7991,6 +8036,38 @@ var ConnectCancelledError = class extends Error {
7991
8036
  }
7992
8037
  };
7993
8038
  var MAX_HANDSHAKE_RETRIES = 2;
8039
+ var RETRY_IN_CONNECT_CODES = /* @__PURE__ */ new Set(["rate_limited", "auth_unavailable"]);
8040
+ var CHANNEL_CATEGORY_RE = /^[A-Za-z0-9_-]{1,128}$/;
8041
+ var CHANNEL_ROOM_RE = /^[A-Za-z0-9._-]{1,128}$/;
8042
+ var MAX_CHANNEL_LENGTH = 256;
8043
+ function toChannel(category, room) {
8044
+ if (room === void 0) return category;
8045
+ const show = (v) => typeof v === "string" ? JSON.stringify(v) : String(v);
8046
+ if (typeof room !== "string" || !CHANNEL_ROOM_RE.test(room)) {
8047
+ throw new Error(
8048
+ `Invalid room ${show(room)}: use 1-128 characters of letters, digits, "-", "_" or "." (the realtime channel is "<category>:<room>").`
8049
+ );
8050
+ }
8051
+ if (typeof category !== "string" || !CHANNEL_CATEGORY_RE.test(category)) {
8052
+ throw new Error(
8053
+ `Invalid category ${show(category)} for a room subscription: use the base category name (1-128 characters of letters, digits, "-" or "_") and pass the room separately with { room }.`
8054
+ );
8055
+ }
8056
+ const channel = `${category}:${room}`;
8057
+ if (channel.length > MAX_CHANNEL_LENGTH) {
8058
+ throw new Error(
8059
+ `Realtime channel "${channel}" is too long (max ${MAX_CHANNEL_LENGTH} characters including ":").`
8060
+ );
8061
+ }
8062
+ return channel;
8063
+ }
8064
+ function roomOf(channel) {
8065
+ const idx = channel.indexOf(":");
8066
+ return idx > 0 && idx < channel.length - 1 ? channel.slice(idx + 1) : void 0;
8067
+ }
8068
+ function toMemberId(value) {
8069
+ return typeof value === "string" && value !== "" ? value : void 0;
8070
+ }
7994
8071
  function jitter(max) {
7995
8072
  return Math.floor(Math.random() * Math.max(0, max));
7996
8073
  }
@@ -8211,31 +8288,46 @@ var RealtimeAPI = class {
8211
8288
  }
8212
8289
  /**
8213
8290
  * 카테고리 구독
8291
+ *
8292
+ * `options.room` 을 주면 그 카테고리의 room 하나를 구독한다 (6.7.0+). 서버 채널 이름은
8293
+ * `${category}:${room}` 이고, 구독 객체의 `send` / `getHistory` / `unsubscribe` / `onMessage`, 재연결 뒤
8294
+ * 다시 구독하기는 모두 이 채널 이름으로 동작한다(`subscription.channel`).
8295
+ *
8296
+ * @example
8297
+ * ```typescript
8298
+ * const stage = await client.realtime.subscribe('concert', { room: 'show-1025' })
8299
+ * stage.channel // 'concert:show-1025'
8300
+ * stage.room // 'show-1025'
8301
+ * stage.info.isPublisher // room 인가 함수가 publisher: true 를 돌려준 로그인 멤버면 true
8302
+ * ```
8214
8303
  */
8215
8304
  async subscribe(category, options = {}) {
8305
+ const channel = toChannel(category, options.room);
8216
8306
  this.assertBidirectional("subscribe");
8217
8307
  if (this.state !== "connected") {
8218
8308
  throw new Error("Not connected. Call connect() first.");
8219
8309
  }
8220
8310
  const requestId = this.generateRequestId();
8221
8311
  const response = await this.sendRequest({
8222
- category,
8312
+ category: channel,
8223
8313
  action: "subscribe",
8224
8314
  request_id: requestId
8225
8315
  });
8226
8316
  const info = toCategoryInfo(response);
8227
8317
  const handlers = [];
8228
- this.subscriptions.set(category, { info, handlers });
8318
+ this.subscriptions.set(channel, { info, handlers });
8229
8319
  const subscription = {
8230
8320
  info,
8321
+ channel,
8322
+ room: roomOf(channel),
8231
8323
  send: async (data, sendOptions) => {
8232
- await this.sendMessage(category, data, sendOptions);
8324
+ await this.sendMessage(channel, data, sendOptions);
8233
8325
  },
8234
8326
  getHistory: async (limit) => {
8235
- return this.getHistory(category, limit ?? options.historyLimit);
8327
+ return this.getHistory(channel, limit ?? options.historyLimit);
8236
8328
  },
8237
8329
  unsubscribe: async () => {
8238
- await this.unsubscribe(category);
8330
+ await this.unsubscribe(channel);
8239
8331
  },
8240
8332
  onMessage: (handler) => {
8241
8333
  handlers.push(handler);
@@ -8249,6 +8341,7 @@ var RealtimeAPI = class {
8249
8341
  }
8250
8342
  /**
8251
8343
  * 구독 해제
8344
+ * @param category 카테고리 이름. room 을 구독했으면 채널 이름(`category:room`, `subscription.channel`)
8252
8345
  */
8253
8346
  async unsubscribe(category) {
8254
8347
  if (this.state !== "connected") {
@@ -8264,7 +8357,7 @@ var RealtimeAPI = class {
8264
8357
  }
8265
8358
  /**
8266
8359
  * 메시지 전송
8267
- * @param category 카테고리 이름
8360
+ * @param category 카테고리 이름. room 을 구독했으면 채널 이름(`category:room`, `subscription.channel`)
8268
8361
  * @param data 전송할 데이터
8269
8362
  * @param options 전송 옵션 (includeSelf: 발신자도 메시지 수신 여부, 기본값 true)
8270
8363
  */
@@ -8285,6 +8378,7 @@ var RealtimeAPI = class {
8285
8378
  }
8286
8379
  /**
8287
8380
  * 히스토리 조회
8381
+ * @param category 카테고리 이름. room 을 구독했으면 채널 이름(`category:room`, `subscription.channel`)
8288
8382
  */
8289
8383
  async getHistory(category, limit) {
8290
8384
  this.assertBidirectional("getHistory");
@@ -8304,6 +8398,7 @@ var RealtimeAPI = class {
8304
8398
  id: m.id,
8305
8399
  category: m.category,
8306
8400
  from: m.from,
8401
+ fromMember: toMemberId(m.from_member),
8307
8402
  data: m.data,
8308
8403
  sentAt: m.sent_at
8309
8404
  })),
@@ -8993,7 +9088,9 @@ var RealtimeAPI = class {
8993
9088
  * 한국 사용자는 미국(LAX)을 거친 ~270ms 로 붙었다(platform-issue 01a15d9d 의 273ms). 응답 헤더는
8994
9089
  * HttpClient 가 평소처럼 학습한다. 광고 대상이 아니면(해외, 킬 스위치) 빈 응답이라 원래 소켓
8995
9090
  * URL 로 간다. 실패하거나 느리면(1.5초) 그냥 넘어간다 — 최적화일 뿐 연결의 전제가 아니다.
8996
- * 세션당 한 번만 묻는다.
9091
+ * 인스턴스당 한 번만 묻고, 같은 baseUrl 의 인스턴스끼리는 진행 중인 조회와 배운 결과를 함께 쓴다
9092
+ * (6.7.0+, HttpClient.primeEdgeHints 참고 — 한 페이지의 인스턴스 10개가 동시에 물어 대부분 1.5초를 넘기던
9093
+ * platform-issue 01a15e81).
8997
9094
  */
8998
9095
  async primeDirectRealtime() {
8999
9096
  if (this.directRealtimePrimed) return;
@@ -9012,12 +9109,17 @@ var RealtimeAPI = class {
9012
9109
  /**
9013
9110
  * 첫 연결을 연결 제한 시간(`timeout`) 안에서 다시 시도한다 (6.6.0+, platform-issue 01a15e24).
9014
9111
  *
9015
- * - 서버가 재시도 시각을 알려 준 거부(`retryAfterMs`, 예: 한 IP 에서 연결이 몰린 `rate_limited`):
9016
- * 그 시각에 지터를 더해 다시 붙는다. 공연 시작처럼 한꺼번에 들어올 때 앱이 재시도를 짜지 않아도 된다.
9112
+ * - 서버가 재시도 시각을 알려 준 거부(`retryAfterMs`, 예: 한 IP 에서 연결이 몰린 `rate_limited`, 인증 서버가
9113
+ * 잠깐 닿지 않는 `auth_unavailable`): 그 시각에 지터를 더해 다시 붙는다. 공연 시작처럼 한꺼번에 들어올 때
9114
+ * 앱이 재시도를 짜지 않아도 된다.
9115
+ * - `rate_limited` / `auth_unavailable` 인데 재시도 시각이 없으면(사유 프레임 없이 close 코드만 왔거나 구버전
9116
+ * 서버) 지수 백오프로 다시 한다. `auth_unavailable` 은 6.6.0 까지 바로 던져, 인증 서버가 몇 초 흔들리는
9117
+ * 동안 들어온 관객이 전부 연결에 실패했다(6.7.0, platform-issue 01a15e81).
9017
9118
  * - 이유 없는 핸드셰이크 실패: 두 번까지 백오프 후 다시 한다. 네트워크가 WebSocket 을 실제로 막으면
9018
9119
  * 매번 실패하고, 앞단의 일시 거부는 다시 하면 붙는다 — 한 번 실패로 SSE 폴백하던 것이 연결 속도
9019
9120
  * 제한을 "네트워크 차단" 으로 오진한 원인이었다.
9020
- * - 그 밖의 거부(자격증명, 동시접속 한도 등)와 시간 초과는 바로 던진다.
9121
+ * - 그 밖의 거부(자격증명, 동시접속 한도 등)와 시간 초과는 바로 던진다. 다시 시도할 시간이 남지 않아도
9122
+ * 마지막 거부를 그대로 던진다(`retryAfterMs` 가 있으면 앱이 그 시각에 다시 부를 수 있다).
9021
9123
  */
9022
9124
  async connectWithRetry() {
9023
9125
  const generation = this.connectGeneration;
@@ -9031,10 +9133,10 @@ var RealtimeAPI = class {
9031
9133
  return;
9032
9134
  } catch (e) {
9033
9135
  let wait = null;
9034
- if (e instanceof RealtimeConnectionError) {
9035
- if (e.retryable && e.retryAfterMs !== void 0) {
9136
+ if (e instanceof RealtimeConnectionError && e.retryable) {
9137
+ if (e.retryAfterMs !== void 0) {
9036
9138
  wait = e.retryAfterMs + jitter(Math.min(100 * 2 ** attempt, 3e3));
9037
- } else if (e.code === "rate_limited") {
9139
+ } else if (RETRY_IN_CONNECT_CODES.has(e.code)) {
9038
9140
  const base = Math.min(500 * 2 ** (attempt - 1), 8e3);
9039
9141
  wait = base / 2 + jitter(base / 2);
9040
9142
  }
@@ -9265,7 +9367,9 @@ var RealtimeAPI = class {
9265
9367
  const message = {
9266
9368
  id: data.id,
9267
9369
  category: data.category,
9370
+ // from 은 클라이언트가 정한 client_id 라 믿을 수 없다. 보낸 사람은 fromMember(서버 검증)로 본다.
9268
9371
  from: data.from,
9372
+ fromMember: toMemberId(data.from_member),
9269
9373
  data: data.data,
9270
9374
  sentAt: data.sent_at,
9271
9375
  fromPublisher: data.from_publisher === true
@@ -11125,8 +11229,8 @@ var SupportAPI = class {
11125
11229
  }
11126
11230
  };
11127
11231
  function detectSdkVersion() {
11128
- if (typeof __SDK_VERSION__ !== "undefined" && __SDK_VERSION__) {
11129
- return __SDK_VERSION__;
11232
+ if ("6.8.0") {
11233
+ return "6.8.0";
11130
11234
  }
11131
11235
  return "unknown";
11132
11236
  }
@@ -12718,6 +12822,63 @@ function isConsolePlatformToken(token) {
12718
12822
  const role = decodeJwtPayload(token)?.role;
12719
12823
  return role === "User" || role === "AdminInvite";
12720
12824
  }
12825
+ var EDGE_HINTS_TTL_MS = 10 * 6e4;
12826
+ var sharedEdgeHints = /* @__PURE__ */ new Map();
12827
+ function lookupEdgeHints(baseUrl, timeoutMs) {
12828
+ const cached = sharedEdgeHints.get(baseUrl);
12829
+ if (cached && Date.now() < cached.expiresAt) return cached.promise;
12830
+ const entry = {
12831
+ promise: fetchEdgeHintAdverts(baseUrl, timeoutMs),
12832
+ // 진행 중에는 만료되지 않는다 — 시간 제한(withAbort) 때문에 timeoutMs 안에 반드시 끝난다. 늦게 합류한
12833
+ // 인스턴스는 남은 시간만 기다린다.
12834
+ expiresAt: Number.POSITIVE_INFINITY
12835
+ };
12836
+ sharedEdgeHints.set(baseUrl, entry);
12837
+ entry.promise.then(
12838
+ () => {
12839
+ entry.expiresAt = Date.now() + EDGE_HINTS_TTL_MS;
12840
+ },
12841
+ () => {
12842
+ if (sharedEdgeHints.get(baseUrl) === entry) {
12843
+ sharedEdgeHints.delete(baseUrl);
12844
+ }
12845
+ }
12846
+ );
12847
+ return entry.promise;
12848
+ }
12849
+ async function fetchEdgeHintAdverts(baseUrl, timeoutMs) {
12850
+ const { signal, cleanup } = createTimeoutController({ timeout: timeoutMs });
12851
+ try {
12852
+ const response = await withAbort(
12853
+ fetch(`${baseUrl}/v1/public/edge-hints`, {
12854
+ method: "GET",
12855
+ credentials: "omit",
12856
+ signal
12857
+ }),
12858
+ signal
12859
+ );
12860
+ if (!response.ok) {
12861
+ throw new Error(`edge-hints HTTP ${response.status}`);
12862
+ }
12863
+ const header = {
12864
+ direct: response.headers?.get("X-CB-Direct-Base") || null,
12865
+ realtime: response.headers?.get("X-CB-Realtime-Direct-Base") || null
12866
+ };
12867
+ let body;
12868
+ try {
12869
+ body = await withAbort(response.json(), signal);
12870
+ } catch (err) {
12871
+ if (header.direct || header.realtime) return [header];
12872
+ throw err;
12873
+ }
12874
+ return [
12875
+ header,
12876
+ { direct: body?.direct_base, realtime: body?.realtime_direct_base }
12877
+ ];
12878
+ } finally {
12879
+ cleanup();
12880
+ }
12881
+ }
12721
12882
  var HttpClient = class {
12722
12883
  constructor(config) {
12723
12884
  this.isRefreshing = false;
@@ -13523,28 +13684,18 @@ var HttpClient = class {
13523
13684
  * 먼저 보내 Cloudflare 를 두 번 왕복하고, 앱의 "로컬 개발 origin 허용" 이 꺼진 localhost 페이지에서는
13524
13685
  * 키 기반 opt-in 에 막혀 브라우저만 direct 를 못 썼다(Node 는 썼다 — platform-issue 01a15e24).
13525
13686
  * 서버는 이 경로를 키 없이 열어 두고 같은 값을 본문으로도 준다. 본문을 읽으므로 CORS
13526
- * Expose-Headers 에도 기대지 않는다. 실패는 조용히 무시한다 — direct 는 최적화일 뿐이다.
13687
+ * Expose-Headers 에도 기대지 않는다. 실패는 호출자에게 던지고 호출자(RealtimeAPI)가 조용히 넘어간다 —
13688
+ * direct 는 최적화일 뿐이다.
13689
+ *
13690
+ * 조회 자체는 같은 baseUrl 의 인스턴스끼리 함께 쓴다(6.7.0+, {@link sharedEdgeHints} 참고). 진행 중인
13691
+ * 조회가 있으면 새로 보내지 않고 기다리며, 성공한 결과는 잠시 다시 쓴다. 받아들일지는 이 인스턴스가 정한다 —
13692
+ * 이미 배웠거나(`directRealtimeBaseUrl`) direct 를 포기했으면(`directRealtimeDisabled`) 바꾸지 않는다.
13527
13693
  */
13528
13694
  async primeEdgeHints(timeoutMs) {
13529
13695
  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();
13696
+ const adverts = await lookupEdgeHints(this.config.baseUrl, timeoutMs);
13697
+ for (const advert of adverts) {
13698
+ this.adoptDirectBases(advert.direct, advert.realtime);
13548
13699
  }
13549
13700
  }
13550
13701
  /**