connectbase-client 6.13.0 → 6.15.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 +52 -0
- package/README.md +17 -1
- package/dist/cli.js +1 -1
- package/dist/connect-base.umd.js +5 -5
- package/dist/index.d.mts +72 -5
- package/dist/index.d.ts +72 -5
- package/dist/index.js +399 -64
- package/dist/index.mjs +398 -64
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -118,9 +118,9 @@ var AdsAPI = class {
|
|
|
118
118
|
|
|
119
119
|
// src/types/error.ts
|
|
120
120
|
var ApiError = class extends Error {
|
|
121
|
-
constructor(
|
|
121
|
+
constructor(statusCode2, message, code, details) {
|
|
122
122
|
super(message);
|
|
123
|
-
this.statusCode =
|
|
123
|
+
this.statusCode = statusCode2;
|
|
124
124
|
this.name = "ApiError";
|
|
125
125
|
this.code = code;
|
|
126
126
|
this.details = details;
|
|
@@ -238,7 +238,7 @@ var RealtimeConnectionError = class extends Error {
|
|
|
238
238
|
this.name = "RealtimeConnectionError";
|
|
239
239
|
this.code = init.code || "unknown";
|
|
240
240
|
this.closeCode = init.closeCode;
|
|
241
|
-
this.retryable = init.retryable ?? isRetryableRealtimeRejection(this.code);
|
|
241
|
+
this.retryable = init.retryable ?? isRetryableRealtimeRejection(this.code, init.closeCode);
|
|
242
242
|
this.details = init.details;
|
|
243
243
|
this.retryAfterMs = init.retryAfterMs;
|
|
244
244
|
}
|
|
@@ -255,8 +255,8 @@ var RealtimeConnectionError = class extends Error {
|
|
|
255
255
|
};
|
|
256
256
|
}
|
|
257
257
|
};
|
|
258
|
-
function isRetryableRealtimeRejection(code) {
|
|
259
|
-
return code === "connection_limit_exceeded" || code === "server_at_capacity" || code === "rate_limited" || code === "auth_unavailable" || code === "internal_error";
|
|
258
|
+
function isRetryableRealtimeRejection(code, closeCode) {
|
|
259
|
+
return closeCode === 4429 || closeCode === 4500 || closeCode === 4503 || code === "connection_limit_exceeded" || code === "rate_limit_exceeded" || code === "server_at_capacity" || code === "rate_limited" || code === "auth_unavailable" || code === "internal_error" || code === "invalid_ws_ticket" || code === "ws_ticket_unavailable";
|
|
260
260
|
}
|
|
261
261
|
var RealtimeRequestError = class extends Error {
|
|
262
262
|
constructor(init) {
|
|
@@ -3646,9 +3646,107 @@ var HttpClient = class {
|
|
|
3646
3646
|
}
|
|
3647
3647
|
};
|
|
3648
3648
|
|
|
3649
|
+
// src/core/ws-ticket.ts
|
|
3650
|
+
var WsTicketError = class extends Error {
|
|
3651
|
+
constructor(init) {
|
|
3652
|
+
super(
|
|
3653
|
+
init.message || init.code || `WebSocket ticket request failed (${init.status})`
|
|
3654
|
+
);
|
|
3655
|
+
this.name = "WsTicketError";
|
|
3656
|
+
this.status = init.status;
|
|
3657
|
+
this.code = init.code || "ws_ticket_rejected";
|
|
3658
|
+
this.retryAfterMs = init.retryAfterMs;
|
|
3659
|
+
}
|
|
3660
|
+
};
|
|
3661
|
+
var DEFAULT_TICKET_TIMEOUT_MS = 5e3;
|
|
3662
|
+
var unsupported = /* @__PURE__ */ new Set();
|
|
3663
|
+
function supportKey(url, kind) {
|
|
3664
|
+
try {
|
|
3665
|
+
const u = new URL(url);
|
|
3666
|
+
return `${u.origin}|${kind}`;
|
|
3667
|
+
} catch {
|
|
3668
|
+
return `${url}|${kind}`;
|
|
3669
|
+
}
|
|
3670
|
+
}
|
|
3671
|
+
async function fetchWsTicket(url, kind, credential, options = {}) {
|
|
3672
|
+
const key = supportKey(url, kind);
|
|
3673
|
+
if (unsupported.has(key)) return null;
|
|
3674
|
+
const headers = {};
|
|
3675
|
+
if (credential.kind === "access_token") {
|
|
3676
|
+
headers.Authorization = `Bearer ${credential.value}`;
|
|
3677
|
+
} else {
|
|
3678
|
+
headers["X-Public-Key"] = credential.value;
|
|
3679
|
+
}
|
|
3680
|
+
const { signal, cleanup } = createTimeoutController({
|
|
3681
|
+
timeout: Math.max(
|
|
3682
|
+
1,
|
|
3683
|
+
Math.min(
|
|
3684
|
+
options.timeoutMs ?? DEFAULT_TICKET_TIMEOUT_MS,
|
|
3685
|
+
DEFAULT_TICKET_TIMEOUT_MS
|
|
3686
|
+
)
|
|
3687
|
+
),
|
|
3688
|
+
signal: options.signal
|
|
3689
|
+
});
|
|
3690
|
+
let response;
|
|
3691
|
+
try {
|
|
3692
|
+
response = await withAbort(
|
|
3693
|
+
fetch(url, { method: "POST", headers, credentials: "omit", signal }),
|
|
3694
|
+
signal
|
|
3695
|
+
);
|
|
3696
|
+
} catch (err) {
|
|
3697
|
+
cleanup();
|
|
3698
|
+
if (options.signal?.aborted) throw err;
|
|
3699
|
+
return null;
|
|
3700
|
+
}
|
|
3701
|
+
try {
|
|
3702
|
+
if (response.ok) {
|
|
3703
|
+
const body2 = await response.json().catch(() => null);
|
|
3704
|
+
return typeof body2?.ticket === "string" && body2.ticket !== "" ? body2.ticket : null;
|
|
3705
|
+
}
|
|
3706
|
+
if (response.status === 404 || response.status === 405 || response.status === 501) {
|
|
3707
|
+
unsupported.add(key);
|
|
3708
|
+
return null;
|
|
3709
|
+
}
|
|
3710
|
+
if (response.status >= 500) return null;
|
|
3711
|
+
const body = await response.json().catch(() => null);
|
|
3712
|
+
const raw = typeof body?.error === "string" ? body.error : void 0;
|
|
3713
|
+
const isCode = raw !== void 0 && /^[a-z0-9_]+$/.test(raw);
|
|
3714
|
+
const code = isCode ? raw : statusCode(response.status);
|
|
3715
|
+
const message = typeof body?.message === "string" ? body.message : raw;
|
|
3716
|
+
const retryAfterMs = typeof body?.retry_after_ms === "number" && body.retry_after_ms > 0 ? body.retry_after_ms : void 0;
|
|
3717
|
+
throw new WsTicketError({
|
|
3718
|
+
status: response.status,
|
|
3719
|
+
code,
|
|
3720
|
+
message,
|
|
3721
|
+
retryAfterMs
|
|
3722
|
+
});
|
|
3723
|
+
} finally {
|
|
3724
|
+
cleanup();
|
|
3725
|
+
}
|
|
3726
|
+
}
|
|
3727
|
+
function statusCode(status) {
|
|
3728
|
+
switch (status) {
|
|
3729
|
+
case 401:
|
|
3730
|
+
return "unauthorized";
|
|
3731
|
+
case 402:
|
|
3732
|
+
return "quota_exceeded";
|
|
3733
|
+
case 403:
|
|
3734
|
+
return "forbidden";
|
|
3735
|
+
case 429:
|
|
3736
|
+
return "rate_limited";
|
|
3737
|
+
default:
|
|
3738
|
+
return "ws_ticket_rejected";
|
|
3739
|
+
}
|
|
3740
|
+
}
|
|
3741
|
+
function ticketUrl(base, path) {
|
|
3742
|
+
const httpBase = base.replace(/^ws(s?):\/\//, "http$1://").replace(/\/+$/, "");
|
|
3743
|
+
return `${httpBase}${path}`;
|
|
3744
|
+
}
|
|
3745
|
+
|
|
3649
3746
|
// src/api/database.ts
|
|
3650
3747
|
var REALTIME_TOKEN_MIN_VALIDITY_SEC = 600;
|
|
3651
3748
|
var REALTIME_CONNECT_TOKEN_MIN_VALIDITY_SEC = 60;
|
|
3749
|
+
var REALTIME_CONNECT_TIMEOUT_MS = 15e3;
|
|
3652
3750
|
var REALTIME_AUTH_REFRESH_RETRY_MS = 3e4;
|
|
3653
3751
|
var REALTIME_CLOSING_NOTICE_CODES = /* @__PURE__ */ new Set([
|
|
3654
3752
|
"TOKEN_EXPIRED",
|
|
@@ -3656,6 +3754,20 @@ var REALTIME_CLOSING_NOTICE_CODES = /* @__PURE__ */ new Set([
|
|
|
3656
3754
|
]);
|
|
3657
3755
|
var CLOSE_CODE_TOKEN_EXPIRED = 4401;
|
|
3658
3756
|
var CLOSE_CODE_MEMBER_REVOKED = 4403;
|
|
3757
|
+
function toRealtimeRejection(msg) {
|
|
3758
|
+
if (msg.type !== "error" || msg.request_id || typeof msg.close_code !== "number") {
|
|
3759
|
+
return null;
|
|
3760
|
+
}
|
|
3761
|
+
const details = msg.details;
|
|
3762
|
+
return new RealtimeConnectionError({
|
|
3763
|
+
code: typeof msg.code === "string" ? msg.code : void 0,
|
|
3764
|
+
message: typeof msg.message === "string" ? msg.message : void 0,
|
|
3765
|
+
closeCode: msg.close_code,
|
|
3766
|
+
retryable: typeof msg.retryable === "boolean" ? msg.retryable : void 0,
|
|
3767
|
+
details: details && typeof details === "object" && !Array.isArray(details) ? details : void 0,
|
|
3768
|
+
retryAfterMs: typeof msg.retry_after_ms === "number" && msg.retry_after_ms > 0 ? msg.retry_after_ms : void 0
|
|
3769
|
+
});
|
|
3770
|
+
}
|
|
3659
3771
|
var DatabaseAPI = class {
|
|
3660
3772
|
constructor(http) {
|
|
3661
3773
|
this.realtimeWs = null;
|
|
@@ -4593,23 +4705,59 @@ var DatabaseAPI = class {
|
|
|
4593
4705
|
);
|
|
4594
4706
|
if (fresh) opts.accessToken = fresh;
|
|
4595
4707
|
}
|
|
4596
|
-
|
|
4708
|
+
let ticket;
|
|
4709
|
+
try {
|
|
4710
|
+
ticket = await this.realtimeTicket();
|
|
4711
|
+
} catch (e) {
|
|
4712
|
+
if (e instanceof WsTicketError) {
|
|
4713
|
+
const err = new RealtimeConnectionError({
|
|
4714
|
+
code: e.code,
|
|
4715
|
+
message: e.message,
|
|
4716
|
+
closeCode: 4e3 + e.status,
|
|
4717
|
+
retryAfterMs: e.retryAfterMs
|
|
4718
|
+
});
|
|
4719
|
+
this.handleRealtimeRejection(err);
|
|
4720
|
+
throw err;
|
|
4721
|
+
}
|
|
4722
|
+
throw e;
|
|
4723
|
+
}
|
|
4724
|
+
return this.doRealtimeConnect(ticket);
|
|
4725
|
+
}
|
|
4726
|
+
/**
|
|
4727
|
+
* 연결 시도 한 번에 쓸 1회용 티켓을 받는다(core/ws-ticket.ts). 브라우저 WebSocket 은 헤더를 못 붙여 예전에는 멤버 토큰을
|
|
4728
|
+
* URL(`?access_token=`)에 실었다 — 이제 헤더로 티켓을 받아 `?ticket=` 만 싣는다. 서버가 티켓을 모르거나(구버전) 잠시 못
|
|
4729
|
+
* 주면 null 이고 예전처럼 붙는다. cross-app OAuth 토큰도 같은 경로다(서버가 티켓에서 토큰을 되살려 같은 검증을 한다).
|
|
4730
|
+
*/
|
|
4731
|
+
async realtimeTicket() {
|
|
4732
|
+
const opts = this.realtimeOptions;
|
|
4733
|
+
if (!opts?.accessToken) return null;
|
|
4734
|
+
const baseUrl = opts.dataServerUrl || this.http.getBaseUrl();
|
|
4735
|
+
return fetchWsTicket(
|
|
4736
|
+
ticketUrl(baseUrl, "/v1/database/realtime/ws-ticket"),
|
|
4737
|
+
"data.realtime",
|
|
4738
|
+
{ kind: "access_token", value: opts.accessToken },
|
|
4739
|
+
{ timeoutMs: REALTIME_CONNECT_TIMEOUT_MS }
|
|
4740
|
+
);
|
|
4597
4741
|
}
|
|
4598
|
-
doRealtimeConnect() {
|
|
4742
|
+
doRealtimeConnect(ticket = null) {
|
|
4599
4743
|
if (!this.realtimeOptions)
|
|
4600
4744
|
return Promise.reject(new Error("No realtime options"));
|
|
4601
4745
|
this.setRealtimeState("connecting");
|
|
4602
4746
|
const baseUrl = this.realtimeOptions.dataServerUrl || this.http.getBaseUrl();
|
|
4603
4747
|
const wsUrl = baseUrl.replace(/^http/, "ws");
|
|
4604
|
-
let url = `${wsUrl}/v1/database/realtime/ws?access_token=${encodeURIComponent(this.realtimeOptions.accessToken)}`;
|
|
4748
|
+
let url = ticket ? `${wsUrl}/v1/database/realtime/ws?ticket=${encodeURIComponent(ticket)}` : `${wsUrl}/v1/database/realtime/ws?access_token=${encodeURIComponent(this.realtimeOptions.accessToken)}`;
|
|
4605
4749
|
this.realtimeTokenRefresh = this.http.canRefreshMemberToken(
|
|
4606
4750
|
this.realtimeOptions.accessToken
|
|
4607
4751
|
);
|
|
4608
4752
|
if (this.realtimeTokenRefresh) url += "&token_refresh=1";
|
|
4753
|
+
url += "&reject_mode=close";
|
|
4609
4754
|
return new Promise((resolve, reject) => {
|
|
4610
4755
|
try {
|
|
4611
|
-
|
|
4756
|
+
const ws = new WebSocket(url);
|
|
4757
|
+
this.realtimeWs = ws;
|
|
4612
4758
|
let settled = false;
|
|
4759
|
+
let accepted = false;
|
|
4760
|
+
let rejection = null;
|
|
4613
4761
|
const connectTimeout = setTimeout(() => {
|
|
4614
4762
|
if (!settled) {
|
|
4615
4763
|
settled = true;
|
|
@@ -4620,8 +4768,9 @@ var DatabaseAPI = class {
|
|
|
4620
4768
|
this.setRealtimeState("disconnected");
|
|
4621
4769
|
reject(new Error("Connection timeout"));
|
|
4622
4770
|
}
|
|
4623
|
-
},
|
|
4624
|
-
|
|
4771
|
+
}, REALTIME_CONNECT_TIMEOUT_MS);
|
|
4772
|
+
const accept = () => {
|
|
4773
|
+
accepted = true;
|
|
4625
4774
|
if (!settled) {
|
|
4626
4775
|
settled = true;
|
|
4627
4776
|
clearTimeout(connectTimeout);
|
|
@@ -4634,15 +4783,28 @@ var DatabaseAPI = class {
|
|
|
4634
4783
|
this.resubscribeAll();
|
|
4635
4784
|
resolve();
|
|
4636
4785
|
};
|
|
4637
|
-
|
|
4786
|
+
ws.onopen = () => {
|
|
4787
|
+
ws.send(JSON.stringify({ type: "ping", timestamp: Date.now() }));
|
|
4788
|
+
};
|
|
4789
|
+
ws.onmessage = (event) => {
|
|
4790
|
+
let msg;
|
|
4638
4791
|
try {
|
|
4639
|
-
|
|
4640
|
-
this.handleRealtimeMessage(msg);
|
|
4792
|
+
msg = JSON.parse(event.data);
|
|
4641
4793
|
} catch {
|
|
4642
4794
|
this.debugLog("Failed to parse realtime message");
|
|
4795
|
+
return;
|
|
4643
4796
|
}
|
|
4797
|
+
if (!accepted) {
|
|
4798
|
+
const rejected = toRealtimeRejection(msg);
|
|
4799
|
+
if (rejected) {
|
|
4800
|
+
rejection = rejected;
|
|
4801
|
+
return;
|
|
4802
|
+
}
|
|
4803
|
+
accept();
|
|
4804
|
+
}
|
|
4805
|
+
this.handleRealtimeMessage(msg);
|
|
4644
4806
|
};
|
|
4645
|
-
|
|
4807
|
+
ws.onclose = (event) => {
|
|
4646
4808
|
this.debugLog(
|
|
4647
4809
|
`Database realtime disconnected (code=${event?.code}, reason=${event?.reason})`
|
|
4648
4810
|
);
|
|
@@ -4651,12 +4813,25 @@ var DatabaseAPI = class {
|
|
|
4651
4813
|
this.clearRealtimeAuthRetry();
|
|
4652
4814
|
const notice = this.realtimeCloseNotice;
|
|
4653
4815
|
this.realtimeCloseNotice = null;
|
|
4816
|
+
const code = event?.code;
|
|
4817
|
+
if (!accepted && code >= 4e3 && code <= 4999) {
|
|
4818
|
+
const err = rejection ?? new RealtimeConnectionError({
|
|
4819
|
+
code: event.reason || void 0,
|
|
4820
|
+
closeCode: code
|
|
4821
|
+
});
|
|
4822
|
+
if (!settled) {
|
|
4823
|
+
settled = true;
|
|
4824
|
+
clearTimeout(connectTimeout);
|
|
4825
|
+
reject(err);
|
|
4826
|
+
}
|
|
4827
|
+
this.handleRealtimeRejection(err);
|
|
4828
|
+
return;
|
|
4829
|
+
}
|
|
4654
4830
|
if (!settled) {
|
|
4655
4831
|
settled = true;
|
|
4656
4832
|
clearTimeout(connectTimeout);
|
|
4657
4833
|
reject(new Error("Connection closed during handshake"));
|
|
4658
4834
|
}
|
|
4659
|
-
const code = event?.code;
|
|
4660
4835
|
if (this.realtimeOptions && this.realtimeState !== "disconnected" && (code === CLOSE_CODE_MEMBER_REVOKED || code === CLOSE_CODE_TOKEN_EXPIRED && !this.http.canRefreshMemberToken(
|
|
4661
4836
|
this.realtimeOptions.accessToken
|
|
4662
4837
|
))) {
|
|
@@ -4674,7 +4849,7 @@ var DatabaseAPI = class {
|
|
|
4674
4849
|
this.attemptRealtimeReconnect();
|
|
4675
4850
|
}
|
|
4676
4851
|
};
|
|
4677
|
-
|
|
4852
|
+
ws.onerror = () => {
|
|
4678
4853
|
this.debugLog("Database realtime error");
|
|
4679
4854
|
this.realtimeOnError?.(new Error("WebSocket connection error"));
|
|
4680
4855
|
};
|
|
@@ -4684,6 +4859,21 @@ var DatabaseAPI = class {
|
|
|
4684
4859
|
}
|
|
4685
4860
|
});
|
|
4686
4861
|
}
|
|
4862
|
+
/**
|
|
4863
|
+
* 서버가 연결을 거부했다(티켓 발급 거부, 또는 reject_mode=close 로 받은 업그레이드 거부). cb.realtime 과 같은 규칙이다 —
|
|
4864
|
+
* 다시 해서 될 거부(`retryable`: 일시 장애, 속도 제한, 동시접속 한도, 만료된 티켓)는 서버가 알려 준 시각(`retryAfterMs`)
|
|
4865
|
+
* 이후로 재연결을 이어 가고, 아니면(자격증명, 멤버 폐기, 할당량) 멈추고 `onRealtimeError` 로 알린다. 재시도를 다 써도
|
|
4866
|
+
* 거부되면 마지막 거부를 알린다.
|
|
4867
|
+
*/
|
|
4868
|
+
handleRealtimeRejection(err) {
|
|
4869
|
+
if (!this.realtimeOptions || this.realtimeState === "disconnected") return;
|
|
4870
|
+
if (err.retryable) {
|
|
4871
|
+
this.attemptRealtimeReconnect(err);
|
|
4872
|
+
return;
|
|
4873
|
+
}
|
|
4874
|
+
this.setRealtimeState("disconnected");
|
|
4875
|
+
this.realtimeOnError?.(err);
|
|
4876
|
+
}
|
|
4687
4877
|
/**
|
|
4688
4878
|
* 서버에 subscribe 메시지를 보내고 서버 subscription_id를 반환하는 Promise
|
|
4689
4879
|
*/
|
|
@@ -4856,21 +5046,25 @@ var DatabaseAPI = class {
|
|
|
4856
5046
|
break;
|
|
4857
5047
|
}
|
|
4858
5048
|
}
|
|
4859
|
-
|
|
5049
|
+
/**
|
|
5050
|
+
* 지수 백오프로 다시 붙는다. cause 는 이번 시도를 끝낸 서버의 거부다 — 알려 준 재시도 시각(`retryAfterMs`)보다 일찍 다시
|
|
5051
|
+
* 하지 않고, 재시도를 다 쓰면 일반 오류 대신 그 거부를 알린다.
|
|
5052
|
+
*/
|
|
5053
|
+
attemptRealtimeReconnect(cause) {
|
|
4860
5054
|
const maxRetries = this.realtimeOptions?.maxRetries ?? 5;
|
|
4861
5055
|
const retryInterval = this.realtimeOptions?.retryInterval ?? 1e3;
|
|
4862
5056
|
if (this.realtimeRetryCount >= maxRetries) {
|
|
4863
5057
|
this.setRealtimeState("disconnected");
|
|
4864
5058
|
this.realtimeOnError?.(
|
|
4865
|
-
new Error("Realtime connection lost. Max retries exceeded.")
|
|
5059
|
+
cause ?? new Error("Realtime connection lost. Max retries exceeded.")
|
|
4866
5060
|
);
|
|
4867
5061
|
return;
|
|
4868
5062
|
}
|
|
4869
5063
|
this.setRealtimeState("connecting");
|
|
4870
5064
|
this.realtimeRetryCount++;
|
|
4871
|
-
const delay = Math.
|
|
4872
|
-
retryInterval * 2 ** (this.realtimeRetryCount - 1),
|
|
4873
|
-
|
|
5065
|
+
const delay = Math.max(
|
|
5066
|
+
Math.min(retryInterval * 2 ** (this.realtimeRetryCount - 1), 3e4),
|
|
5067
|
+
cause?.retryAfterMs ?? 0
|
|
4874
5068
|
);
|
|
4875
5069
|
this.debugLog(
|
|
4876
5070
|
`Reconnecting in ${delay}ms (attempt ${this.realtimeRetryCount}/${maxRetries})`
|
|
@@ -5161,7 +5355,7 @@ var EndpointAPI = class {
|
|
|
5161
5355
|
);
|
|
5162
5356
|
}
|
|
5163
5357
|
const baseUrl = this.http.getBaseUrl().replace(/\/+$/, "");
|
|
5164
|
-
const
|
|
5358
|
+
const ticketUrl2 = `${baseUrl}/v1/proxy/${encodeURIComponent(label)}/ws-ticket`;
|
|
5165
5359
|
const ticketHeaders = new Headers();
|
|
5166
5360
|
const pk = this.http.getPublicKey();
|
|
5167
5361
|
if (!pk) {
|
|
@@ -5170,7 +5364,7 @@ var EndpointAPI = class {
|
|
|
5170
5364
|
);
|
|
5171
5365
|
}
|
|
5172
5366
|
ticketHeaders.set("X-Public-Key", pk);
|
|
5173
|
-
const ticketRes = await fetch(
|
|
5367
|
+
const ticketRes = await fetch(ticketUrl2, {
|
|
5174
5368
|
method: "POST",
|
|
5175
5369
|
headers: ticketHeaders,
|
|
5176
5370
|
signal: opts.signal
|
|
@@ -5868,20 +6062,54 @@ var GameRoom = class {
|
|
|
5868
6062
|
* 게임 서버에 연결
|
|
5869
6063
|
*/
|
|
5870
6064
|
connect(roomId) {
|
|
5871
|
-
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
|
|
5881
|
-
|
|
5882
|
-
|
|
6065
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
6066
|
+
return Promise.resolve();
|
|
6067
|
+
}
|
|
6068
|
+
if (!this.config.appId) {
|
|
6069
|
+
return Promise.reject(
|
|
6070
|
+
new Error(
|
|
6071
|
+
"cb.game: appId is required to connect. Pass it to `new ConnectBase({ appId })` or per-client via `cb.game.createClient({ appId, clientId })`."
|
|
6072
|
+
)
|
|
6073
|
+
);
|
|
6074
|
+
}
|
|
6075
|
+
if (!this.config.clientId) {
|
|
6076
|
+
return Promise.reject(
|
|
6077
|
+
new Error(
|
|
6078
|
+
"cb.game: clientId is required to build a game connection URL. Pass it via `cb.game.createClient({ appId, clientId })`."
|
|
6079
|
+
)
|
|
6080
|
+
);
|
|
6081
|
+
}
|
|
6082
|
+
return this.connectionTicket().then(
|
|
6083
|
+
(ticket) => this.openSocket(roomId, ticket)
|
|
6084
|
+
);
|
|
6085
|
+
}
|
|
6086
|
+
/**
|
|
6087
|
+
* 연결 시도 한 번에 쓸 1회용 티켓을 받는다(core/ws-ticket.ts). 브라우저 WebSocket 은 헤더를 못 붙여 예전에는 멤버
|
|
6088
|
+
* 토큰(`?token=`)과 공개 키(`?public_key=`)를 URL 에 실었다 — 이제 헤더로 티켓을 받아 `?ticket=` 만 싣는다. 서버가
|
|
6089
|
+
* 티켓을 모르거나(구버전) 잠시 못 주면 null 이고 예전처럼 붙는다. 발급이 자격증명을 거부하면 {@link GameError} 로 던진다.
|
|
6090
|
+
*/
|
|
6091
|
+
async connectionTicket() {
|
|
6092
|
+
const { accessToken, publicKey, appId, gameServerUrl } = this.config;
|
|
6093
|
+
if (!accessToken && !publicKey || !appId || !gameServerUrl) return null;
|
|
6094
|
+
try {
|
|
6095
|
+
return await fetchWsTicket(
|
|
6096
|
+
ticketUrl(
|
|
6097
|
+
gameServerUrl,
|
|
6098
|
+
`/v1/game/${encodeURIComponent(appId)}/ws-ticket`
|
|
6099
|
+
),
|
|
6100
|
+
"game.ws",
|
|
6101
|
+
accessToken ? { kind: "access_token", value: accessToken } : { kind: "public_key", value: publicKey }
|
|
6102
|
+
);
|
|
6103
|
+
} catch (e) {
|
|
6104
|
+
if (e instanceof WsTicketError) {
|
|
6105
|
+
throw new GameError({ code: e.code, message: e.message });
|
|
5883
6106
|
}
|
|
5884
|
-
|
|
6107
|
+
throw e;
|
|
6108
|
+
}
|
|
6109
|
+
}
|
|
6110
|
+
openSocket(roomId, ticket) {
|
|
6111
|
+
return new Promise((resolve, reject) => {
|
|
6112
|
+
const url = this.buildConnectionUrl(roomId, ticket);
|
|
5885
6113
|
this.ws = new WebSocket(url);
|
|
5886
6114
|
const onOpen = () => {
|
|
5887
6115
|
this._isConnected = true;
|
|
@@ -6137,7 +6365,7 @@ var GameRoom = class {
|
|
|
6137
6365
|
});
|
|
6138
6366
|
}
|
|
6139
6367
|
// Private methods
|
|
6140
|
-
buildConnectionUrl(roomId) {
|
|
6368
|
+
buildConnectionUrl(roomId, ticket) {
|
|
6141
6369
|
const baseUrl = this.config.gameServerUrl;
|
|
6142
6370
|
const wsUrl = baseUrl.replace(/^http/, "ws");
|
|
6143
6371
|
const params = new URLSearchParams();
|
|
@@ -6150,11 +6378,15 @@ var GameRoom = class {
|
|
|
6150
6378
|
if (roomId) {
|
|
6151
6379
|
params.set("room_id", roomId);
|
|
6152
6380
|
}
|
|
6153
|
-
if (
|
|
6154
|
-
params.set("
|
|
6155
|
-
}
|
|
6156
|
-
|
|
6157
|
-
|
|
6381
|
+
if (ticket) {
|
|
6382
|
+
params.set("ticket", ticket);
|
|
6383
|
+
} else {
|
|
6384
|
+
if (this.config.publicKey) {
|
|
6385
|
+
params.set("public_key", this.config.publicKey);
|
|
6386
|
+
}
|
|
6387
|
+
if (this.config.accessToken) {
|
|
6388
|
+
params.set("token", this.config.accessToken);
|
|
6389
|
+
}
|
|
6158
6390
|
}
|
|
6159
6391
|
if (!this.config.appId) {
|
|
6160
6392
|
throw new Error(
|
|
@@ -10011,7 +10243,14 @@ var ConnectCancelledError = class extends Error {
|
|
|
10011
10243
|
}
|
|
10012
10244
|
};
|
|
10013
10245
|
var MAX_HANDSHAKE_RETRIES = 2;
|
|
10014
|
-
var RETRY_IN_CONNECT_CODES = /* @__PURE__ */ new Set([
|
|
10246
|
+
var RETRY_IN_CONNECT_CODES = /* @__PURE__ */ new Set([
|
|
10247
|
+
"rate_limited",
|
|
10248
|
+
"auth_unavailable",
|
|
10249
|
+
// 1회용 티켓이 업그레이드에서 거부됐다(만료, 이미 쓰임, 저장소 일시 장애). 다음 시도는 새 티켓을 받거나, 저장소가
|
|
10250
|
+
// 없으면 예전 방식으로 붙는다(core/ws-ticket.ts).
|
|
10251
|
+
"invalid_ws_ticket",
|
|
10252
|
+
"ws_ticket_unavailable"
|
|
10253
|
+
]);
|
|
10015
10254
|
var CONNECTION_TOKEN_MIN_VALIDITY_SEC = 600;
|
|
10016
10255
|
var CONNECT_TOKEN_MIN_VALIDITY_SEC = 60;
|
|
10017
10256
|
var AUTH_REFRESH_RETRY_MS = 3e4;
|
|
@@ -11126,7 +11365,10 @@ var RealtimeAPI = class {
|
|
|
11126
11365
|
for (let attempt = 1; ; attempt++) {
|
|
11127
11366
|
try {
|
|
11128
11367
|
await this.prepareConnectToken();
|
|
11129
|
-
await this.
|
|
11368
|
+
const ticket = await this.connectTicket(
|
|
11369
|
+
Math.max(deadline - Date.now(), 1)
|
|
11370
|
+
);
|
|
11371
|
+
await this.doConnect(Math.max(deadline - Date.now(), 1), ticket);
|
|
11130
11372
|
return;
|
|
11131
11373
|
} catch (e) {
|
|
11132
11374
|
let wait = null;
|
|
@@ -11169,7 +11411,39 @@ var RealtimeAPI = class {
|
|
|
11169
11411
|
this.initialConnecting = false;
|
|
11170
11412
|
}
|
|
11171
11413
|
}
|
|
11172
|
-
|
|
11414
|
+
/**
|
|
11415
|
+
* 연결 시도 한 번에 쓸 1회용 티켓을 받는다(core/ws-ticket.ts). 브라우저 WebSocket 은 헤더를 못 붙여 예전에는 멤버
|
|
11416
|
+
* 토큰과 공개 키를 URL 에 실었다 — 이제 헤더로 티켓을 받아 `?ticket=` 만 싣는다. 티켓은 한 번만 쓰이므로 시도마다
|
|
11417
|
+
* 새로 받는다.
|
|
11418
|
+
*
|
|
11419
|
+
* 서버가 티켓을 모르거나(구버전) 잠시 못 주면 null 이고, doConnect 가 예전처럼 자격증명을 URL 에 싣는다. 발급이
|
|
11420
|
+
* 자격증명을 거부하면 업그레이드 거부와 같은 {@link RealtimeConnectionError} 로 던진다 — 재시도 규칙이 같다.
|
|
11421
|
+
*/
|
|
11422
|
+
async connectTicket(timeoutMs) {
|
|
11423
|
+
const accessToken = this.options.accessToken;
|
|
11424
|
+
const publicKey = accessToken ? void 0 : this.http.getPublicKey();
|
|
11425
|
+
if (!accessToken && !publicKey) return null;
|
|
11426
|
+
try {
|
|
11427
|
+
return await fetchWsTicket(
|
|
11428
|
+
ticketUrl(this.socketUrl, "/v1/realtime/ws-ticket"),
|
|
11429
|
+
"socket.realtime",
|
|
11430
|
+
accessToken ? { kind: "access_token", value: accessToken } : { kind: "public_key", value: publicKey },
|
|
11431
|
+
{ timeoutMs }
|
|
11432
|
+
);
|
|
11433
|
+
} catch (e) {
|
|
11434
|
+
if (e instanceof WsTicketError) {
|
|
11435
|
+
this.log(`WebSocket ticket rejected: ${e.code} (${e.status})`);
|
|
11436
|
+
throw new RealtimeConnectionError({
|
|
11437
|
+
code: e.code,
|
|
11438
|
+
message: e.message,
|
|
11439
|
+
closeCode: 4e3 + e.status,
|
|
11440
|
+
retryAfterMs: e.retryAfterMs
|
|
11441
|
+
});
|
|
11442
|
+
}
|
|
11443
|
+
throw e;
|
|
11444
|
+
}
|
|
11445
|
+
}
|
|
11446
|
+
async doConnect(timeoutMs = this.options.timeout, ticket = null) {
|
|
11173
11447
|
return new Promise((resolve, reject) => {
|
|
11174
11448
|
this.state = "connecting";
|
|
11175
11449
|
this.notifyStateChange();
|
|
@@ -11178,9 +11452,10 @@ var RealtimeAPI = class {
|
|
|
11178
11452
|
const usingDirect = !!directWsBase;
|
|
11179
11453
|
const wsUrl = (directWsBase ?? this.socketUrl).replace(/^http/, "ws");
|
|
11180
11454
|
let url;
|
|
11455
|
+
const auth = ticket ? `ticket=${encodeURIComponent(ticket)}` : void 0;
|
|
11181
11456
|
this.tokenRefreshNegotiated = false;
|
|
11182
11457
|
if (this.options.accessToken) {
|
|
11183
|
-
url = `${wsUrl}/v1/realtime/auth
|
|
11458
|
+
url = `${wsUrl}/v1/realtime/auth?${auth ?? `access_token=${encodeURIComponent(this.options.accessToken)}`}&client_id=${this.clientId}`;
|
|
11184
11459
|
if (this.http.canRefreshMemberToken(this.options.accessToken)) {
|
|
11185
11460
|
this.tokenRefreshNegotiated = true;
|
|
11186
11461
|
url += "&token_refresh=1";
|
|
@@ -11196,7 +11471,7 @@ var RealtimeAPI = class {
|
|
|
11196
11471
|
reject(error);
|
|
11197
11472
|
return;
|
|
11198
11473
|
}
|
|
11199
|
-
url = `${wsUrl}/v1/realtime/auth
|
|
11474
|
+
url = `${wsUrl}/v1/realtime/auth?${auth ?? `public_key=${encodeURIComponent(publicKey)}`}&client_id=${this.clientId}`;
|
|
11200
11475
|
this.log("Using API Key authentication");
|
|
11201
11476
|
}
|
|
11202
11477
|
if (this.userId) {
|
|
@@ -11676,7 +11951,9 @@ var RealtimeAPI = class {
|
|
|
11676
11951
|
try {
|
|
11677
11952
|
await this.prepareConnectToken();
|
|
11678
11953
|
if (this.state !== "reconnecting") return;
|
|
11679
|
-
await this.
|
|
11954
|
+
const ticket = await this.connectTicket(this.options.timeout);
|
|
11955
|
+
if (this.state !== "reconnecting") return;
|
|
11956
|
+
await this.doConnect(void 0, ticket);
|
|
11680
11957
|
this.log("Reconnected successfully, restoring subscriptions...");
|
|
11681
11958
|
const snapshot = this.pendingRestore;
|
|
11682
11959
|
this.pendingRestore = null;
|
|
@@ -13279,8 +13556,8 @@ var SupportAPI = class {
|
|
|
13279
13556
|
}
|
|
13280
13557
|
};
|
|
13281
13558
|
function detectSdkVersion() {
|
|
13282
|
-
if ("6.
|
|
13283
|
-
return "6.
|
|
13559
|
+
if ("6.15.0") {
|
|
13560
|
+
return "6.15.0";
|
|
13284
13561
|
}
|
|
13285
13562
|
return "unknown";
|
|
13286
13563
|
}
|
|
@@ -14249,9 +14526,35 @@ var WebRTCAPI = class _WebRTCAPI {
|
|
|
14249
14526
|
}
|
|
14250
14527
|
return this.connectWebSocket();
|
|
14251
14528
|
}
|
|
14252
|
-
connectWebSocket() {
|
|
14529
|
+
async connectWebSocket() {
|
|
14530
|
+
const before = this.wsGeneration;
|
|
14531
|
+
const ticket = await this.signalingTicket();
|
|
14532
|
+
if (this.wsGeneration !== before) {
|
|
14533
|
+
throw new Error("\uC5F0\uACB0\uC774 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4");
|
|
14534
|
+
}
|
|
14535
|
+
return this.openSignalingSocket(ticket);
|
|
14536
|
+
}
|
|
14537
|
+
/**
|
|
14538
|
+
* 연결 시도 한 번에 쓸 1회용 티켓을 받는다(core/ws-ticket.ts). 브라우저 WebSocket 은 헤더를 못 붙여 예전에는 멤버 토큰
|
|
14539
|
+
* (`?access_token=`)과 공개 키(`?public_key=`)를 URL 에 실었다 — 이제 헤더로 티켓을 받아 `?ticket=` 만 싣는다. 서버가
|
|
14540
|
+
* 티켓을 모르거나(구버전) 잠시 못 주면 null 이고 예전처럼 붙는다. 발급이 자격증명을 거부하면 그 이유로 던진다.
|
|
14541
|
+
*/
|
|
14542
|
+
async signalingTicket() {
|
|
14543
|
+
const accessToken = this.http.getAccessToken();
|
|
14544
|
+
const publicKey = accessToken ? void 0 : this.http.getPublicKey();
|
|
14545
|
+
if (!accessToken && !publicKey || !this.appId) return null;
|
|
14546
|
+
return fetchWsTicket(
|
|
14547
|
+
ticketUrl(
|
|
14548
|
+
this.webrtcUrl,
|
|
14549
|
+
`/v1/apps/${encodeURIComponent(this.appId)}/signaling/ws-ticket`
|
|
14550
|
+
),
|
|
14551
|
+
"webrtc.signaling",
|
|
14552
|
+
accessToken ? { kind: "access_token", value: accessToken } : { kind: "public_key", value: publicKey }
|
|
14553
|
+
);
|
|
14554
|
+
}
|
|
14555
|
+
openSignalingSocket(ticket) {
|
|
14253
14556
|
return new Promise((resolve, reject) => {
|
|
14254
|
-
const wsUrl = this.buildWebSocketUrl();
|
|
14557
|
+
const wsUrl = this.buildWebSocketUrl(ticket);
|
|
14255
14558
|
const generation = ++this.wsGeneration;
|
|
14256
14559
|
const ws = new WebSocket(wsUrl);
|
|
14257
14560
|
this.ws = ws;
|
|
@@ -14320,12 +14623,14 @@ var WebRTCAPI = class _WebRTCAPI {
|
|
|
14320
14623
|
ws.onerror = null;
|
|
14321
14624
|
ws.onclose = null;
|
|
14322
14625
|
}
|
|
14323
|
-
buildWebSocketUrl() {
|
|
14626
|
+
buildWebSocketUrl(ticket = null) {
|
|
14324
14627
|
const wsBase = this.webrtcUrl.replace("https://", "wss://").replace("http://", "ws://");
|
|
14325
14628
|
const publicKey = this.http.getPublicKey();
|
|
14326
14629
|
const accessToken = this.http.getAccessToken();
|
|
14327
14630
|
let authParam = "";
|
|
14328
|
-
if (
|
|
14631
|
+
if (ticket) {
|
|
14632
|
+
authParam = `ticket=${encodeURIComponent(ticket)}`;
|
|
14633
|
+
} else if (accessToken) {
|
|
14329
14634
|
authParam = `access_token=${encodeURIComponent(accessToken)}`;
|
|
14330
14635
|
} else if (publicKey) {
|
|
14331
14636
|
authParam = `public_key=${encodeURIComponent(publicKey)}`;
|
|
@@ -14983,11 +15288,35 @@ var WebSocketTransport = class {
|
|
|
14983
15288
|
this.onClose = onClose;
|
|
14984
15289
|
this.onError = onError;
|
|
14985
15290
|
}
|
|
14986
|
-
connect() {
|
|
15291
|
+
async connect() {
|
|
15292
|
+
const ticket = await this.connectionTicket();
|
|
15293
|
+
return this.openSocket(ticket);
|
|
15294
|
+
}
|
|
15295
|
+
/**
|
|
15296
|
+
* 연결 시도 한 번에 쓸 1회용 티켓(core/ws-ticket.ts). 서버가 티켓을 모르거나(구버전) 잠시 못 주면 null 이고 예전처럼
|
|
15297
|
+
* 자격증명을 URL 에 싣는다. 발급이 자격증명을 거부하면 {@link GameError} 로 던진다.
|
|
15298
|
+
*/
|
|
15299
|
+
async connectionTicket() {
|
|
15300
|
+
const { accessToken, publicKey, appId } = this.config;
|
|
15301
|
+
if (!accessToken && !publicKey || !appId) return null;
|
|
15302
|
+
const baseUrl = this.config.gameServerUrl || "wss://game.connectbase.world";
|
|
15303
|
+
try {
|
|
15304
|
+
return await fetchWsTicket(
|
|
15305
|
+
ticketUrl(baseUrl, `/v1/game/${encodeURIComponent(appId)}/ws-ticket`),
|
|
15306
|
+
"game.ws",
|
|
15307
|
+
accessToken ? { kind: "access_token", value: accessToken } : { kind: "public_key", value: publicKey }
|
|
15308
|
+
);
|
|
15309
|
+
} catch (e) {
|
|
15310
|
+
if (e instanceof WsTicketError) {
|
|
15311
|
+
throw new GameError({ code: e.code, message: e.message });
|
|
15312
|
+
}
|
|
15313
|
+
throw e;
|
|
15314
|
+
}
|
|
15315
|
+
}
|
|
15316
|
+
openSocket(ticket) {
|
|
14987
15317
|
return new Promise((resolve, reject) => {
|
|
14988
|
-
const url = this.buildUrl();
|
|
14989
15318
|
try {
|
|
14990
|
-
this.ws = new WebSocket(
|
|
15319
|
+
this.ws = new WebSocket(this.buildUrl(ticket));
|
|
14991
15320
|
this.ws.binaryType = "arraybuffer";
|
|
14992
15321
|
} catch (error) {
|
|
14993
15322
|
reject(error);
|
|
@@ -15017,16 +15346,20 @@ var WebSocketTransport = class {
|
|
|
15017
15346
|
this.ws.addEventListener("message", onMessage);
|
|
15018
15347
|
});
|
|
15019
15348
|
}
|
|
15020
|
-
buildUrl() {
|
|
15349
|
+
buildUrl(ticket = null) {
|
|
15021
15350
|
const baseUrl = this.config.gameServerUrl || "wss://game.connectbase.world";
|
|
15022
15351
|
const wsUrl = baseUrl.replace(/^http/, "ws");
|
|
15023
15352
|
const params = new URLSearchParams();
|
|
15024
15353
|
params.set("client_id", requireClientId(this.config.clientId));
|
|
15025
|
-
if (
|
|
15026
|
-
params.set("
|
|
15027
|
-
}
|
|
15028
|
-
|
|
15029
|
-
|
|
15354
|
+
if (ticket) {
|
|
15355
|
+
params.set("ticket", ticket);
|
|
15356
|
+
} else {
|
|
15357
|
+
if (this.config.publicKey) {
|
|
15358
|
+
params.set("public_key", this.config.publicKey);
|
|
15359
|
+
}
|
|
15360
|
+
if (this.config.accessToken) {
|
|
15361
|
+
params.set("token", this.config.accessToken);
|
|
15362
|
+
}
|
|
15030
15363
|
}
|
|
15031
15364
|
if (!this.config.appId) {
|
|
15032
15365
|
throw new Error(
|
|
@@ -15705,6 +16038,7 @@ export {
|
|
|
15705
16038
|
RolesAPI,
|
|
15706
16039
|
SessionManager,
|
|
15707
16040
|
VideoProcessingError,
|
|
16041
|
+
WsTicketError,
|
|
15708
16042
|
index_default as default,
|
|
15709
16043
|
detectInAppBrowser,
|
|
15710
16044
|
escapeToExternalBrowser,
|