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/CHANGELOG.md +111 -0
- package/README.md +57 -4
- package/dist/cli.js +1 -1
- package/dist/connect-base.umd.js +5 -5
- package/dist/index.d.mts +147 -21
- package/dist/index.d.ts +147 -21
- package/dist/index.js +201 -50
- package/dist/index.mjs +201 -50
- package/package.json +1 -1
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.
|
|
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
|
-
*
|
|
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 = {
|
|
7600
|
-
|
|
7601
|
-
|
|
7602
|
-
|
|
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
|
-
|
|
7619
|
-
|
|
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 (등록
|
|
7709
|
+
* @param deviceToken Web Push endpoint URL (등록 응답의 `device_token`)
|
|
7698
7710
|
*/
|
|
7699
7711
|
async unregisterWebPush(deviceToken) {
|
|
7700
|
-
|
|
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(
|
|
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(
|
|
8324
|
+
await this.sendMessage(channel, data, sendOptions);
|
|
8233
8325
|
},
|
|
8234
8326
|
getHistory: async (limit) => {
|
|
8235
|
-
return this.getHistory(
|
|
8327
|
+
return this.getHistory(channel, limit ?? options.historyLimit);
|
|
8236
8328
|
},
|
|
8237
8329
|
unsubscribe: async () => {
|
|
8238
|
-
await this.unsubscribe(
|
|
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.
|
|
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
|
|
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 (
|
|
11129
|
-
return
|
|
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 에도 기대지 않는다. 실패는
|
|
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
|
|
13531
|
-
|
|
13532
|
-
|
|
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
|
/**
|