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.mjs
CHANGED
|
@@ -2022,6 +2022,11 @@ var AuthAPI = class {
|
|
|
2022
2022
|
* role: 'editor',
|
|
2023
2023
|
* custom_data: { level: 5 }
|
|
2024
2024
|
* })
|
|
2025
|
+
*
|
|
2026
|
+
* // 역할 여러 개 (6.7.0+): role 은 주 역할(RLS auth.role), roles 는 추가 역할 목록을 **통째로 바꾼다**
|
|
2027
|
+
* // ([] 면 비우고, 빼면 그대로). auth.roles, hasRole(), 실시간 publisher_roles 는 role + roles 를 함께 본다.
|
|
2028
|
+
* await cb.auth.adminUpdateMember('member-uuid', { roles: ['performer'] })
|
|
2029
|
+
* // → { role: 'admin', roles: ['performer'], effective_roles: ['admin', 'performer'], ... }
|
|
2025
2030
|
* ```
|
|
2026
2031
|
*/
|
|
2027
2032
|
async adminUpdateMember(memberID, fields) {
|
|
@@ -7519,7 +7524,7 @@ var PushAPI = class {
|
|
|
7519
7524
|
/**
|
|
7520
7525
|
* 디바이스 등록 해제
|
|
7521
7526
|
*
|
|
7522
|
-
* @param deviceToken 디바이스 토큰
|
|
7527
|
+
* @param deviceToken 디바이스 토큰 (웹 푸시 기기는 등록 응답의 `device_token`, 즉 endpoint URL)
|
|
7523
7528
|
*
|
|
7524
7529
|
* @example
|
|
7525
7530
|
* ```typescript
|
|
@@ -7528,13 +7533,18 @@ var PushAPI = class {
|
|
|
7528
7533
|
*/
|
|
7529
7534
|
async unregisterDevice(deviceToken) {
|
|
7530
7535
|
const prefix = this.getPublicPrefix();
|
|
7531
|
-
await this.http.
|
|
7536
|
+
await this.http.post(`${prefix}/push/devices/unregister`, {
|
|
7537
|
+
device_token: deviceToken
|
|
7538
|
+
});
|
|
7532
7539
|
}
|
|
7533
7540
|
// ============ Topic Subscription ============
|
|
7534
7541
|
/**
|
|
7535
7542
|
* 토픽 구독
|
|
7536
7543
|
*
|
|
7537
|
-
*
|
|
7544
|
+
* 토픽은 미리 있어야 한다(콘솔에서 만든다). 없으면 404 로 거부된다.
|
|
7545
|
+
*
|
|
7546
|
+
* @param deviceToken 디바이스 토큰 — `registerDevice` / `registerWebPush` 응답의 `device_token`
|
|
7547
|
+
* (웹 푸시 기기는 endpoint URL. 6.7.0 이하는 웹 기기를 구독시키지 못했다)
|
|
7538
7548
|
* @param topicName 토픽 이름
|
|
7539
7549
|
*
|
|
7540
7550
|
* @example
|
|
@@ -7544,11 +7554,11 @@ var PushAPI = class {
|
|
|
7544
7554
|
*/
|
|
7545
7555
|
async subscribeTopic(deviceToken, topicName) {
|
|
7546
7556
|
const prefix = this.getPublicPrefix();
|
|
7547
|
-
const request = {
|
|
7548
|
-
|
|
7549
|
-
|
|
7550
|
-
|
|
7551
|
-
);
|
|
7557
|
+
const request = {
|
|
7558
|
+
device_token: deviceToken,
|
|
7559
|
+
topic_name: topicName
|
|
7560
|
+
};
|
|
7561
|
+
await this.http.post(`${prefix}/push/topics/subscribe`, request);
|
|
7552
7562
|
}
|
|
7553
7563
|
/**
|
|
7554
7564
|
* 토픽 구독 해제
|
|
@@ -7563,9 +7573,11 @@ var PushAPI = class {
|
|
|
7563
7573
|
*/
|
|
7564
7574
|
async unsubscribeTopic(deviceToken, topicName) {
|
|
7565
7575
|
const prefix = this.getPublicPrefix();
|
|
7566
|
-
|
|
7567
|
-
|
|
7568
|
-
|
|
7576
|
+
const request = {
|
|
7577
|
+
device_token: deviceToken,
|
|
7578
|
+
topic_name: topicName
|
|
7579
|
+
};
|
|
7580
|
+
await this.http.post(`${prefix}/push/topics/unsubscribe`, request);
|
|
7569
7581
|
}
|
|
7570
7582
|
// ============ Web Push ============
|
|
7571
7583
|
/**
|
|
@@ -7642,13 +7654,10 @@ var PushAPI = class {
|
|
|
7642
7654
|
/**
|
|
7643
7655
|
* Web Push 구독 해제
|
|
7644
7656
|
*
|
|
7645
|
-
* @param deviceToken Web Push endpoint URL (등록
|
|
7657
|
+
* @param deviceToken Web Push endpoint URL (등록 응답의 `device_token`)
|
|
7646
7658
|
*/
|
|
7647
7659
|
async unregisterWebPush(deviceToken) {
|
|
7648
|
-
|
|
7649
|
-
await this.http.delete(
|
|
7650
|
-
`${prefix}/push/devices/${encodeURIComponent(deviceToken)}`
|
|
7651
|
-
);
|
|
7660
|
+
await this.unregisterDevice(deviceToken);
|
|
7652
7661
|
}
|
|
7653
7662
|
// ============ Server-side Send (Functions / cb_sk_ 전용) ============
|
|
7654
7663
|
/**
|
|
@@ -7751,6 +7760,42 @@ var PushAPI = class {
|
|
|
7751
7760
|
body
|
|
7752
7761
|
);
|
|
7753
7762
|
}
|
|
7763
|
+
/**
|
|
7764
|
+
* 토픽 만들기 — 서버 코드 전용 (6.8.0+).
|
|
7765
|
+
*
|
|
7766
|
+
* 방마다 쓰는 토픽처럼 서버 코드가 토픽을 만들어 두고, 기기는 그 뒤에 `subscribeTopic` 으로 구독한다. 같은 이름의
|
|
7767
|
+
* 토픽이 이미 있으면 오류 없이 그 토픽을 돌려준다(설정은 바꾸지 않는다) — 트리거가 다시 실행돼도 안전하다.
|
|
7768
|
+
*
|
|
7769
|
+
* Public Key(cb_pk_) 단독 인스턴스로는 호출할 수 없다(공개 키로는 토픽을 만들 수 없다). service_role(ctx.cbAdmin)은
|
|
7770
|
+
* 함수의 management_scopes 에 `push:write` 가 있어야 한다. User Secret Key(cb_sk_), 콘솔 JWT 도 된다.
|
|
7771
|
+
*
|
|
7772
|
+
* @param appId 앱 ID
|
|
7773
|
+
* @param name 토픽 이름 (255바이트 이하, 앞뒤 공백은 지운다)
|
|
7774
|
+
* @param options 표시 이름, 설명
|
|
7775
|
+
* @returns 만들었거나 이미 있던 토픽
|
|
7776
|
+
*
|
|
7777
|
+
* @example
|
|
7778
|
+
* ```typescript
|
|
7779
|
+
* // concerts 트리거 함수: 공연이 생기면 그 방의 알림 토픽을 만들어 둔다
|
|
7780
|
+
* await ctx.cbAdmin.push.createTopic(appId, `cc_${row.room}`, { displayName: row.title })
|
|
7781
|
+
* ```
|
|
7782
|
+
*/
|
|
7783
|
+
async createTopic(appId, name, options = {}) {
|
|
7784
|
+
if (this.http.hasPublicKey() && !this.http.hasJWT()) {
|
|
7785
|
+
throw new Error(
|
|
7786
|
+
"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."
|
|
7787
|
+
);
|
|
7788
|
+
}
|
|
7789
|
+
if (!name || !name.trim()) {
|
|
7790
|
+
throw new Error("cb.push.createTopic(): name \uC774 \uBE44\uC5B4\uC788\uC2B5\uB2C8\uB2E4.");
|
|
7791
|
+
}
|
|
7792
|
+
return this.http.post(`/v1/apps/${appId}/push/topics`, {
|
|
7793
|
+
name,
|
|
7794
|
+
if_not_exists: true,
|
|
7795
|
+
...options.displayName !== void 0 ? { display_name: options.displayName } : {},
|
|
7796
|
+
...options.description !== void 0 ? { description: options.description } : {}
|
|
7797
|
+
});
|
|
7798
|
+
}
|
|
7754
7799
|
/**
|
|
7755
7800
|
* 앱의 푸시 통계(도달/오픈율/클릭율, 디바이스/메시지/토픽 수)를 조회한다.
|
|
7756
7801
|
*
|
|
@@ -7939,6 +7984,38 @@ var ConnectCancelledError = class extends Error {
|
|
|
7939
7984
|
}
|
|
7940
7985
|
};
|
|
7941
7986
|
var MAX_HANDSHAKE_RETRIES = 2;
|
|
7987
|
+
var RETRY_IN_CONNECT_CODES = /* @__PURE__ */ new Set(["rate_limited", "auth_unavailable"]);
|
|
7988
|
+
var CHANNEL_CATEGORY_RE = /^[A-Za-z0-9_-]{1,128}$/;
|
|
7989
|
+
var CHANNEL_ROOM_RE = /^[A-Za-z0-9._-]{1,128}$/;
|
|
7990
|
+
var MAX_CHANNEL_LENGTH = 256;
|
|
7991
|
+
function toChannel(category, room) {
|
|
7992
|
+
if (room === void 0) return category;
|
|
7993
|
+
const show = (v) => typeof v === "string" ? JSON.stringify(v) : String(v);
|
|
7994
|
+
if (typeof room !== "string" || !CHANNEL_ROOM_RE.test(room)) {
|
|
7995
|
+
throw new Error(
|
|
7996
|
+
`Invalid room ${show(room)}: use 1-128 characters of letters, digits, "-", "_" or "." (the realtime channel is "<category>:<room>").`
|
|
7997
|
+
);
|
|
7998
|
+
}
|
|
7999
|
+
if (typeof category !== "string" || !CHANNEL_CATEGORY_RE.test(category)) {
|
|
8000
|
+
throw new Error(
|
|
8001
|
+
`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 }.`
|
|
8002
|
+
);
|
|
8003
|
+
}
|
|
8004
|
+
const channel = `${category}:${room}`;
|
|
8005
|
+
if (channel.length > MAX_CHANNEL_LENGTH) {
|
|
8006
|
+
throw new Error(
|
|
8007
|
+
`Realtime channel "${channel}" is too long (max ${MAX_CHANNEL_LENGTH} characters including ":").`
|
|
8008
|
+
);
|
|
8009
|
+
}
|
|
8010
|
+
return channel;
|
|
8011
|
+
}
|
|
8012
|
+
function roomOf(channel) {
|
|
8013
|
+
const idx = channel.indexOf(":");
|
|
8014
|
+
return idx > 0 && idx < channel.length - 1 ? channel.slice(idx + 1) : void 0;
|
|
8015
|
+
}
|
|
8016
|
+
function toMemberId(value) {
|
|
8017
|
+
return typeof value === "string" && value !== "" ? value : void 0;
|
|
8018
|
+
}
|
|
7942
8019
|
function jitter(max) {
|
|
7943
8020
|
return Math.floor(Math.random() * Math.max(0, max));
|
|
7944
8021
|
}
|
|
@@ -8159,31 +8236,46 @@ var RealtimeAPI = class {
|
|
|
8159
8236
|
}
|
|
8160
8237
|
/**
|
|
8161
8238
|
* 카테고리 구독
|
|
8239
|
+
*
|
|
8240
|
+
* `options.room` 을 주면 그 카테고리의 room 하나를 구독한다 (6.7.0+). 서버 채널 이름은
|
|
8241
|
+
* `${category}:${room}` 이고, 구독 객체의 `send` / `getHistory` / `unsubscribe` / `onMessage`, 재연결 뒤
|
|
8242
|
+
* 다시 구독하기는 모두 이 채널 이름으로 동작한다(`subscription.channel`).
|
|
8243
|
+
*
|
|
8244
|
+
* @example
|
|
8245
|
+
* ```typescript
|
|
8246
|
+
* const stage = await client.realtime.subscribe('concert', { room: 'show-1025' })
|
|
8247
|
+
* stage.channel // 'concert:show-1025'
|
|
8248
|
+
* stage.room // 'show-1025'
|
|
8249
|
+
* stage.info.isPublisher // room 인가 함수가 publisher: true 를 돌려준 로그인 멤버면 true
|
|
8250
|
+
* ```
|
|
8162
8251
|
*/
|
|
8163
8252
|
async subscribe(category, options = {}) {
|
|
8253
|
+
const channel = toChannel(category, options.room);
|
|
8164
8254
|
this.assertBidirectional("subscribe");
|
|
8165
8255
|
if (this.state !== "connected") {
|
|
8166
8256
|
throw new Error("Not connected. Call connect() first.");
|
|
8167
8257
|
}
|
|
8168
8258
|
const requestId = this.generateRequestId();
|
|
8169
8259
|
const response = await this.sendRequest({
|
|
8170
|
-
category,
|
|
8260
|
+
category: channel,
|
|
8171
8261
|
action: "subscribe",
|
|
8172
8262
|
request_id: requestId
|
|
8173
8263
|
});
|
|
8174
8264
|
const info = toCategoryInfo(response);
|
|
8175
8265
|
const handlers = [];
|
|
8176
|
-
this.subscriptions.set(
|
|
8266
|
+
this.subscriptions.set(channel, { info, handlers });
|
|
8177
8267
|
const subscription = {
|
|
8178
8268
|
info,
|
|
8269
|
+
channel,
|
|
8270
|
+
room: roomOf(channel),
|
|
8179
8271
|
send: async (data, sendOptions) => {
|
|
8180
|
-
await this.sendMessage(
|
|
8272
|
+
await this.sendMessage(channel, data, sendOptions);
|
|
8181
8273
|
},
|
|
8182
8274
|
getHistory: async (limit) => {
|
|
8183
|
-
return this.getHistory(
|
|
8275
|
+
return this.getHistory(channel, limit ?? options.historyLimit);
|
|
8184
8276
|
},
|
|
8185
8277
|
unsubscribe: async () => {
|
|
8186
|
-
await this.unsubscribe(
|
|
8278
|
+
await this.unsubscribe(channel);
|
|
8187
8279
|
},
|
|
8188
8280
|
onMessage: (handler) => {
|
|
8189
8281
|
handlers.push(handler);
|
|
@@ -8197,6 +8289,7 @@ var RealtimeAPI = class {
|
|
|
8197
8289
|
}
|
|
8198
8290
|
/**
|
|
8199
8291
|
* 구독 해제
|
|
8292
|
+
* @param category 카테고리 이름. room 을 구독했으면 채널 이름(`category:room`, `subscription.channel`)
|
|
8200
8293
|
*/
|
|
8201
8294
|
async unsubscribe(category) {
|
|
8202
8295
|
if (this.state !== "connected") {
|
|
@@ -8212,7 +8305,7 @@ var RealtimeAPI = class {
|
|
|
8212
8305
|
}
|
|
8213
8306
|
/**
|
|
8214
8307
|
* 메시지 전송
|
|
8215
|
-
* @param category 카테고리 이름
|
|
8308
|
+
* @param category 카테고리 이름. room 을 구독했으면 채널 이름(`category:room`, `subscription.channel`)
|
|
8216
8309
|
* @param data 전송할 데이터
|
|
8217
8310
|
* @param options 전송 옵션 (includeSelf: 발신자도 메시지 수신 여부, 기본값 true)
|
|
8218
8311
|
*/
|
|
@@ -8233,6 +8326,7 @@ var RealtimeAPI = class {
|
|
|
8233
8326
|
}
|
|
8234
8327
|
/**
|
|
8235
8328
|
* 히스토리 조회
|
|
8329
|
+
* @param category 카테고리 이름. room 을 구독했으면 채널 이름(`category:room`, `subscription.channel`)
|
|
8236
8330
|
*/
|
|
8237
8331
|
async getHistory(category, limit) {
|
|
8238
8332
|
this.assertBidirectional("getHistory");
|
|
@@ -8252,6 +8346,7 @@ var RealtimeAPI = class {
|
|
|
8252
8346
|
id: m.id,
|
|
8253
8347
|
category: m.category,
|
|
8254
8348
|
from: m.from,
|
|
8349
|
+
fromMember: toMemberId(m.from_member),
|
|
8255
8350
|
data: m.data,
|
|
8256
8351
|
sentAt: m.sent_at
|
|
8257
8352
|
})),
|
|
@@ -8941,7 +9036,9 @@ var RealtimeAPI = class {
|
|
|
8941
9036
|
* 한국 사용자는 미국(LAX)을 거친 ~270ms 로 붙었다(platform-issue 01a15d9d 의 273ms). 응답 헤더는
|
|
8942
9037
|
* HttpClient 가 평소처럼 학습한다. 광고 대상이 아니면(해외, 킬 스위치) 빈 응답이라 원래 소켓
|
|
8943
9038
|
* URL 로 간다. 실패하거나 느리면(1.5초) 그냥 넘어간다 — 최적화일 뿐 연결의 전제가 아니다.
|
|
8944
|
-
*
|
|
9039
|
+
* 인스턴스당 한 번만 묻고, 같은 baseUrl 의 인스턴스끼리는 진행 중인 조회와 배운 결과를 함께 쓴다
|
|
9040
|
+
* (6.7.0+, HttpClient.primeEdgeHints 참고 — 한 페이지의 인스턴스 10개가 동시에 물어 대부분 1.5초를 넘기던
|
|
9041
|
+
* platform-issue 01a15e81).
|
|
8945
9042
|
*/
|
|
8946
9043
|
async primeDirectRealtime() {
|
|
8947
9044
|
if (this.directRealtimePrimed) return;
|
|
@@ -8960,12 +9057,17 @@ var RealtimeAPI = class {
|
|
|
8960
9057
|
/**
|
|
8961
9058
|
* 첫 연결을 연결 제한 시간(`timeout`) 안에서 다시 시도한다 (6.6.0+, platform-issue 01a15e24).
|
|
8962
9059
|
*
|
|
8963
|
-
* - 서버가 재시도 시각을 알려 준 거부(`retryAfterMs`, 예: 한 IP 에서 연결이 몰린 `rate_limited
|
|
8964
|
-
* 그 시각에 지터를 더해 다시 붙는다. 공연 시작처럼 한꺼번에 들어올 때
|
|
9060
|
+
* - 서버가 재시도 시각을 알려 준 거부(`retryAfterMs`, 예: 한 IP 에서 연결이 몰린 `rate_limited`, 인증 서버가
|
|
9061
|
+
* 잠깐 닿지 않는 `auth_unavailable`): 그 시각에 지터를 더해 다시 붙는다. 공연 시작처럼 한꺼번에 들어올 때
|
|
9062
|
+
* 앱이 재시도를 짜지 않아도 된다.
|
|
9063
|
+
* - `rate_limited` / `auth_unavailable` 인데 재시도 시각이 없으면(사유 프레임 없이 close 코드만 왔거나 구버전
|
|
9064
|
+
* 서버) 지수 백오프로 다시 한다. `auth_unavailable` 은 6.6.0 까지 바로 던져, 인증 서버가 몇 초 흔들리는
|
|
9065
|
+
* 동안 들어온 관객이 전부 연결에 실패했다(6.7.0, platform-issue 01a15e81).
|
|
8965
9066
|
* - 이유 없는 핸드셰이크 실패: 두 번까지 백오프 후 다시 한다. 네트워크가 WebSocket 을 실제로 막으면
|
|
8966
9067
|
* 매번 실패하고, 앞단의 일시 거부는 다시 하면 붙는다 — 한 번 실패로 SSE 폴백하던 것이 연결 속도
|
|
8967
9068
|
* 제한을 "네트워크 차단" 으로 오진한 원인이었다.
|
|
8968
|
-
* - 그 밖의 거부(자격증명, 동시접속 한도 등)와 시간 초과는 바로 던진다.
|
|
9069
|
+
* - 그 밖의 거부(자격증명, 동시접속 한도 등)와 시간 초과는 바로 던진다. 다시 시도할 시간이 남지 않아도
|
|
9070
|
+
* 마지막 거부를 그대로 던진다(`retryAfterMs` 가 있으면 앱이 그 시각에 다시 부를 수 있다).
|
|
8969
9071
|
*/
|
|
8970
9072
|
async connectWithRetry() {
|
|
8971
9073
|
const generation = this.connectGeneration;
|
|
@@ -8979,10 +9081,10 @@ var RealtimeAPI = class {
|
|
|
8979
9081
|
return;
|
|
8980
9082
|
} catch (e) {
|
|
8981
9083
|
let wait = null;
|
|
8982
|
-
if (e instanceof RealtimeConnectionError) {
|
|
8983
|
-
if (e.
|
|
9084
|
+
if (e instanceof RealtimeConnectionError && e.retryable) {
|
|
9085
|
+
if (e.retryAfterMs !== void 0) {
|
|
8984
9086
|
wait = e.retryAfterMs + jitter(Math.min(100 * 2 ** attempt, 3e3));
|
|
8985
|
-
} else if (e.code
|
|
9087
|
+
} else if (RETRY_IN_CONNECT_CODES.has(e.code)) {
|
|
8986
9088
|
const base = Math.min(500 * 2 ** (attempt - 1), 8e3);
|
|
8987
9089
|
wait = base / 2 + jitter(base / 2);
|
|
8988
9090
|
}
|
|
@@ -9213,7 +9315,9 @@ var RealtimeAPI = class {
|
|
|
9213
9315
|
const message = {
|
|
9214
9316
|
id: data.id,
|
|
9215
9317
|
category: data.category,
|
|
9318
|
+
// from 은 클라이언트가 정한 client_id 라 믿을 수 없다. 보낸 사람은 fromMember(서버 검증)로 본다.
|
|
9216
9319
|
from: data.from,
|
|
9320
|
+
fromMember: toMemberId(data.from_member),
|
|
9217
9321
|
data: data.data,
|
|
9218
9322
|
sentAt: data.sent_at,
|
|
9219
9323
|
fromPublisher: data.from_publisher === true
|
|
@@ -11073,8 +11177,8 @@ var SupportAPI = class {
|
|
|
11073
11177
|
}
|
|
11074
11178
|
};
|
|
11075
11179
|
function detectSdkVersion() {
|
|
11076
|
-
if (
|
|
11077
|
-
return
|
|
11180
|
+
if ("6.8.0") {
|
|
11181
|
+
return "6.8.0";
|
|
11078
11182
|
}
|
|
11079
11183
|
return "unknown";
|
|
11080
11184
|
}
|
|
@@ -12666,6 +12770,63 @@ function isConsolePlatformToken(token) {
|
|
|
12666
12770
|
const role = decodeJwtPayload(token)?.role;
|
|
12667
12771
|
return role === "User" || role === "AdminInvite";
|
|
12668
12772
|
}
|
|
12773
|
+
var EDGE_HINTS_TTL_MS = 10 * 6e4;
|
|
12774
|
+
var sharedEdgeHints = /* @__PURE__ */ new Map();
|
|
12775
|
+
function lookupEdgeHints(baseUrl, timeoutMs) {
|
|
12776
|
+
const cached = sharedEdgeHints.get(baseUrl);
|
|
12777
|
+
if (cached && Date.now() < cached.expiresAt) return cached.promise;
|
|
12778
|
+
const entry = {
|
|
12779
|
+
promise: fetchEdgeHintAdverts(baseUrl, timeoutMs),
|
|
12780
|
+
// 진행 중에는 만료되지 않는다 — 시간 제한(withAbort) 때문에 timeoutMs 안에 반드시 끝난다. 늦게 합류한
|
|
12781
|
+
// 인스턴스는 남은 시간만 기다린다.
|
|
12782
|
+
expiresAt: Number.POSITIVE_INFINITY
|
|
12783
|
+
};
|
|
12784
|
+
sharedEdgeHints.set(baseUrl, entry);
|
|
12785
|
+
entry.promise.then(
|
|
12786
|
+
() => {
|
|
12787
|
+
entry.expiresAt = Date.now() + EDGE_HINTS_TTL_MS;
|
|
12788
|
+
},
|
|
12789
|
+
() => {
|
|
12790
|
+
if (sharedEdgeHints.get(baseUrl) === entry) {
|
|
12791
|
+
sharedEdgeHints.delete(baseUrl);
|
|
12792
|
+
}
|
|
12793
|
+
}
|
|
12794
|
+
);
|
|
12795
|
+
return entry.promise;
|
|
12796
|
+
}
|
|
12797
|
+
async function fetchEdgeHintAdverts(baseUrl, timeoutMs) {
|
|
12798
|
+
const { signal, cleanup } = createTimeoutController({ timeout: timeoutMs });
|
|
12799
|
+
try {
|
|
12800
|
+
const response = await withAbort(
|
|
12801
|
+
fetch(`${baseUrl}/v1/public/edge-hints`, {
|
|
12802
|
+
method: "GET",
|
|
12803
|
+
credentials: "omit",
|
|
12804
|
+
signal
|
|
12805
|
+
}),
|
|
12806
|
+
signal
|
|
12807
|
+
);
|
|
12808
|
+
if (!response.ok) {
|
|
12809
|
+
throw new Error(`edge-hints HTTP ${response.status}`);
|
|
12810
|
+
}
|
|
12811
|
+
const header = {
|
|
12812
|
+
direct: response.headers?.get("X-CB-Direct-Base") || null,
|
|
12813
|
+
realtime: response.headers?.get("X-CB-Realtime-Direct-Base") || null
|
|
12814
|
+
};
|
|
12815
|
+
let body;
|
|
12816
|
+
try {
|
|
12817
|
+
body = await withAbort(response.json(), signal);
|
|
12818
|
+
} catch (err) {
|
|
12819
|
+
if (header.direct || header.realtime) return [header];
|
|
12820
|
+
throw err;
|
|
12821
|
+
}
|
|
12822
|
+
return [
|
|
12823
|
+
header,
|
|
12824
|
+
{ direct: body?.direct_base, realtime: body?.realtime_direct_base }
|
|
12825
|
+
];
|
|
12826
|
+
} finally {
|
|
12827
|
+
cleanup();
|
|
12828
|
+
}
|
|
12829
|
+
}
|
|
12669
12830
|
var HttpClient = class {
|
|
12670
12831
|
constructor(config) {
|
|
12671
12832
|
this.isRefreshing = false;
|
|
@@ -13471,28 +13632,18 @@ var HttpClient = class {
|
|
|
13471
13632
|
* 먼저 보내 Cloudflare 를 두 번 왕복하고, 앱의 "로컬 개발 origin 허용" 이 꺼진 localhost 페이지에서는
|
|
13472
13633
|
* 키 기반 opt-in 에 막혀 브라우저만 direct 를 못 썼다(Node 는 썼다 — platform-issue 01a15e24).
|
|
13473
13634
|
* 서버는 이 경로를 키 없이 열어 두고 같은 값을 본문으로도 준다. 본문을 읽으므로 CORS
|
|
13474
|
-
* Expose-Headers 에도 기대지 않는다. 실패는
|
|
13635
|
+
* Expose-Headers 에도 기대지 않는다. 실패는 호출자에게 던지고 호출자(RealtimeAPI)가 조용히 넘어간다 —
|
|
13636
|
+
* direct 는 최적화일 뿐이다.
|
|
13637
|
+
*
|
|
13638
|
+
* 조회 자체는 같은 baseUrl 의 인스턴스끼리 함께 쓴다(6.7.0+, {@link sharedEdgeHints} 참고). 진행 중인
|
|
13639
|
+
* 조회가 있으면 새로 보내지 않고 기다리며, 성공한 결과는 잠시 다시 쓴다. 받아들일지는 이 인스턴스가 정한다 —
|
|
13640
|
+
* 이미 배웠거나(`directRealtimeBaseUrl`) direct 를 포기했으면(`directRealtimeDisabled`) 바꾸지 않는다.
|
|
13475
13641
|
*/
|
|
13476
13642
|
async primeEdgeHints(timeoutMs) {
|
|
13477
13643
|
if (this.directRealtimeBaseUrl || this.directRealtimeDisabled) return;
|
|
13478
|
-
const
|
|
13479
|
-
|
|
13480
|
-
|
|
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();
|
|
13644
|
+
const adverts = await lookupEdgeHints(this.config.baseUrl, timeoutMs);
|
|
13645
|
+
for (const advert of adverts) {
|
|
13646
|
+
this.adoptDirectBases(advert.direct, advert.realtime);
|
|
13496
13647
|
}
|
|
13497
13648
|
}
|
|
13498
13649
|
/**
|