connectbase-client 6.14.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 +25 -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 +23 -4
- package/dist/index.d.ts +23 -4
- package/dist/index.js +83 -24
- package/dist/index.mjs +83 -24
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -153,7 +153,8 @@ declare class GameError extends Error {
|
|
|
153
153
|
toJSON(): Record<string, unknown>;
|
|
154
154
|
}
|
|
155
155
|
/**
|
|
156
|
-
* 실시간(WebSocket) 연결 거부 코드. socket-server 가 연결을 받지
|
|
156
|
+
* 실시간(WebSocket) 연결 거부 코드. 서버(`cb.realtime` 의 socket-server, `cb.database` 실시간의 data-server)가 연결을 받지
|
|
157
|
+
* 않은 이유이고, 대응이 코드마다 다르다.
|
|
157
158
|
*
|
|
158
159
|
* | 코드 | 원인 | 재시도 | 권장 대응 |
|
|
159
160
|
* |------|------|--------|-----------|
|
|
@@ -169,8 +170,15 @@ declare class GameError extends Error {
|
|
|
169
170
|
* | `invalid_ws_ticket` | 1회용 연결 티켓이 만료됐거나 이미 쓰였다 | 가능 | `connect()` 가 새 티켓으로 스스로 다시 붙는다 |
|
|
170
171
|
* | `ws_ticket_unavailable` | 서버의 티켓 저장소 일시 장애 | 가능 | `connect()` 가 다시 시도하고, 티켓을 못 받으면 예전 방식으로 붙는다 |
|
|
171
172
|
* | `query_credential_rejected` | 서버가 URL 쿼리 자격증명을 받지 않도록 설정됐다 | 불가 | 1회용 티켓으로 붙는 SDK 로 올린다 |
|
|
173
|
+
* | `ambiguous_credentials` | 티켓과 다른 자격증명을 함께 보냈다 | 불가 | 직접 짠 클라이언트라면 둘 중 하나만 보낸다 |
|
|
174
|
+
* | `rate_limit_exceeded` | 요청 속도 제한 (`cb.database` 실시간) | 가능 | `retryAfterMs` 가 있으면 그 뒤에 스스로 다시 붙는다 |
|
|
175
|
+
* | `invalid_token` | cross-app OAuth 토큰 검증 실패 (`cb.database` 실시간) | 불가 | 새 토큰으로 다시 연결 |
|
|
176
|
+
* | `member_revoked` | 멤버 정지, 삭제, 세션 전체 종료 | 불가 | 다시 로그인. `message` 에 앱이 적은 정지 사유가 있다 |
|
|
177
|
+
* | `token_expired` | 연결 중 멤버 토큰이 만료됐고 갱신할 수 없다 | 불가 | 새 토큰으로 다시 연결 |
|
|
178
|
+
*
|
|
179
|
+
* 코드를 모르는 거부도 close 코드가 4429, 4500, 4503 이면 재시도 가능으로 본다.
|
|
172
180
|
*/
|
|
173
|
-
type RealtimeConnectionErrorCode = "connection_limit_exceeded" | "quota_exceeded" | "server_at_capacity" | "rate_limited" | "auth_unavailable" | "unauthorized" | "forbidden" | "invalid_user_id" | "internal_error" | "invalid_ws_ticket" | "ws_ticket_unavailable" | "query_credential_rejected" | (string & {});
|
|
181
|
+
type RealtimeConnectionErrorCode = "connection_limit_exceeded" | "quota_exceeded" | "server_at_capacity" | "rate_limited" | "auth_unavailable" | "unauthorized" | "forbidden" | "invalid_user_id" | "internal_error" | "invalid_ws_ticket" | "ws_ticket_unavailable" | "query_credential_rejected" | "ambiguous_credentials" | "rate_limit_exceeded" | "invalid_token" | "member_revoked" | "token_expired" | (string & {});
|
|
174
182
|
/**
|
|
175
183
|
* 서버가 이유를 밝히고 실시간 연결을 거부했을 때 `cb.realtime.connect()` 가 던지는 에러.
|
|
176
184
|
*
|
|
@@ -3720,6 +3728,13 @@ declare class DatabaseAPI {
|
|
|
3720
3728
|
*/
|
|
3721
3729
|
private realtimeTicket;
|
|
3722
3730
|
private doRealtimeConnect;
|
|
3731
|
+
/**
|
|
3732
|
+
* 서버가 연결을 거부했다(티켓 발급 거부, 또는 reject_mode=close 로 받은 업그레이드 거부). cb.realtime 과 같은 규칙이다 —
|
|
3733
|
+
* 다시 해서 될 거부(`retryable`: 일시 장애, 속도 제한, 동시접속 한도, 만료된 티켓)는 서버가 알려 준 시각(`retryAfterMs`)
|
|
3734
|
+
* 이후로 재연결을 이어 가고, 아니면(자격증명, 멤버 폐기, 할당량) 멈추고 `onRealtimeError` 로 알린다. 재시도를 다 써도
|
|
3735
|
+
* 거부되면 마지막 거부를 알린다.
|
|
3736
|
+
*/
|
|
3737
|
+
private handleRealtimeRejection;
|
|
3723
3738
|
/**
|
|
3724
3739
|
* 서버에 subscribe 메시지를 보내고 서버 subscription_id를 반환하는 Promise
|
|
3725
3740
|
*/
|
|
@@ -3729,6 +3744,10 @@ declare class DatabaseAPI {
|
|
|
3729
3744
|
*/
|
|
3730
3745
|
private resubscribeAll;
|
|
3731
3746
|
private handleRealtimeMessage;
|
|
3747
|
+
/**
|
|
3748
|
+
* 지수 백오프로 다시 붙는다. cause 는 이번 시도를 끝낸 서버의 거부다 — 알려 준 재시도 시각(`retryAfterMs`)보다 일찍 다시
|
|
3749
|
+
* 하지 않고, 재시도를 다 쓰면 일반 오류 대신 그 거부를 알린다.
|
|
3750
|
+
*/
|
|
3732
3751
|
private attemptRealtimeReconnect;
|
|
3733
3752
|
private startRealtimePing;
|
|
3734
3753
|
private stopRealtimePing;
|
|
@@ -10982,7 +11001,7 @@ interface ReportPlatformBugRequest {
|
|
|
10982
11001
|
/** SDK 버전 override (자동 감지 실패 시). */
|
|
10983
11002
|
sdkVersion?: string;
|
|
10984
11003
|
/** SDK 플랫폼 override. */
|
|
10985
|
-
sdkPlatform?: "web" | "node" | "
|
|
11004
|
+
sdkPlatform?: "web" | "node" | "cli" | "mcp" | "other";
|
|
10986
11005
|
/** 환경 (production/staging/development). 기본 unknown. */
|
|
10987
11006
|
environment?: "production" | "staging" | "development" | "unknown";
|
|
10988
11007
|
}
|
|
@@ -11019,7 +11038,7 @@ interface PlatformIssueDetail {
|
|
|
11019
11038
|
status: "open" | "triaged" | "in_progress" | "resolved" | "wontfix" | "duplicate";
|
|
11020
11039
|
resolution_note?: string;
|
|
11021
11040
|
sdk_version?: string;
|
|
11022
|
-
sdk_platform?: "web" | "node" | "
|
|
11041
|
+
sdk_platform?: "web" | "node" | "cli" | "mcp" | "other";
|
|
11023
11042
|
environment?: "production" | "staging" | "development" | "unknown";
|
|
11024
11043
|
external_links?: Array<{
|
|
11025
11044
|
kind: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -153,7 +153,8 @@ declare class GameError extends Error {
|
|
|
153
153
|
toJSON(): Record<string, unknown>;
|
|
154
154
|
}
|
|
155
155
|
/**
|
|
156
|
-
* 실시간(WebSocket) 연결 거부 코드. socket-server 가 연결을 받지
|
|
156
|
+
* 실시간(WebSocket) 연결 거부 코드. 서버(`cb.realtime` 의 socket-server, `cb.database` 실시간의 data-server)가 연결을 받지
|
|
157
|
+
* 않은 이유이고, 대응이 코드마다 다르다.
|
|
157
158
|
*
|
|
158
159
|
* | 코드 | 원인 | 재시도 | 권장 대응 |
|
|
159
160
|
* |------|------|--------|-----------|
|
|
@@ -169,8 +170,15 @@ declare class GameError extends Error {
|
|
|
169
170
|
* | `invalid_ws_ticket` | 1회용 연결 티켓이 만료됐거나 이미 쓰였다 | 가능 | `connect()` 가 새 티켓으로 스스로 다시 붙는다 |
|
|
170
171
|
* | `ws_ticket_unavailable` | 서버의 티켓 저장소 일시 장애 | 가능 | `connect()` 가 다시 시도하고, 티켓을 못 받으면 예전 방식으로 붙는다 |
|
|
171
172
|
* | `query_credential_rejected` | 서버가 URL 쿼리 자격증명을 받지 않도록 설정됐다 | 불가 | 1회용 티켓으로 붙는 SDK 로 올린다 |
|
|
173
|
+
* | `ambiguous_credentials` | 티켓과 다른 자격증명을 함께 보냈다 | 불가 | 직접 짠 클라이언트라면 둘 중 하나만 보낸다 |
|
|
174
|
+
* | `rate_limit_exceeded` | 요청 속도 제한 (`cb.database` 실시간) | 가능 | `retryAfterMs` 가 있으면 그 뒤에 스스로 다시 붙는다 |
|
|
175
|
+
* | `invalid_token` | cross-app OAuth 토큰 검증 실패 (`cb.database` 실시간) | 불가 | 새 토큰으로 다시 연결 |
|
|
176
|
+
* | `member_revoked` | 멤버 정지, 삭제, 세션 전체 종료 | 불가 | 다시 로그인. `message` 에 앱이 적은 정지 사유가 있다 |
|
|
177
|
+
* | `token_expired` | 연결 중 멤버 토큰이 만료됐고 갱신할 수 없다 | 불가 | 새 토큰으로 다시 연결 |
|
|
178
|
+
*
|
|
179
|
+
* 코드를 모르는 거부도 close 코드가 4429, 4500, 4503 이면 재시도 가능으로 본다.
|
|
172
180
|
*/
|
|
173
|
-
type RealtimeConnectionErrorCode = "connection_limit_exceeded" | "quota_exceeded" | "server_at_capacity" | "rate_limited" | "auth_unavailable" | "unauthorized" | "forbidden" | "invalid_user_id" | "internal_error" | "invalid_ws_ticket" | "ws_ticket_unavailable" | "query_credential_rejected" | (string & {});
|
|
181
|
+
type RealtimeConnectionErrorCode = "connection_limit_exceeded" | "quota_exceeded" | "server_at_capacity" | "rate_limited" | "auth_unavailable" | "unauthorized" | "forbidden" | "invalid_user_id" | "internal_error" | "invalid_ws_ticket" | "ws_ticket_unavailable" | "query_credential_rejected" | "ambiguous_credentials" | "rate_limit_exceeded" | "invalid_token" | "member_revoked" | "token_expired" | (string & {});
|
|
174
182
|
/**
|
|
175
183
|
* 서버가 이유를 밝히고 실시간 연결을 거부했을 때 `cb.realtime.connect()` 가 던지는 에러.
|
|
176
184
|
*
|
|
@@ -3720,6 +3728,13 @@ declare class DatabaseAPI {
|
|
|
3720
3728
|
*/
|
|
3721
3729
|
private realtimeTicket;
|
|
3722
3730
|
private doRealtimeConnect;
|
|
3731
|
+
/**
|
|
3732
|
+
* 서버가 연결을 거부했다(티켓 발급 거부, 또는 reject_mode=close 로 받은 업그레이드 거부). cb.realtime 과 같은 규칙이다 —
|
|
3733
|
+
* 다시 해서 될 거부(`retryable`: 일시 장애, 속도 제한, 동시접속 한도, 만료된 티켓)는 서버가 알려 준 시각(`retryAfterMs`)
|
|
3734
|
+
* 이후로 재연결을 이어 가고, 아니면(자격증명, 멤버 폐기, 할당량) 멈추고 `onRealtimeError` 로 알린다. 재시도를 다 써도
|
|
3735
|
+
* 거부되면 마지막 거부를 알린다.
|
|
3736
|
+
*/
|
|
3737
|
+
private handleRealtimeRejection;
|
|
3723
3738
|
/**
|
|
3724
3739
|
* 서버에 subscribe 메시지를 보내고 서버 subscription_id를 반환하는 Promise
|
|
3725
3740
|
*/
|
|
@@ -3729,6 +3744,10 @@ declare class DatabaseAPI {
|
|
|
3729
3744
|
*/
|
|
3730
3745
|
private resubscribeAll;
|
|
3731
3746
|
private handleRealtimeMessage;
|
|
3747
|
+
/**
|
|
3748
|
+
* 지수 백오프로 다시 붙는다. cause 는 이번 시도를 끝낸 서버의 거부다 — 알려 준 재시도 시각(`retryAfterMs`)보다 일찍 다시
|
|
3749
|
+
* 하지 않고, 재시도를 다 쓰면 일반 오류 대신 그 거부를 알린다.
|
|
3750
|
+
*/
|
|
3732
3751
|
private attemptRealtimeReconnect;
|
|
3733
3752
|
private startRealtimePing;
|
|
3734
3753
|
private stopRealtimePing;
|
|
@@ -10982,7 +11001,7 @@ interface ReportPlatformBugRequest {
|
|
|
10982
11001
|
/** SDK 버전 override (자동 감지 실패 시). */
|
|
10983
11002
|
sdkVersion?: string;
|
|
10984
11003
|
/** SDK 플랫폼 override. */
|
|
10985
|
-
sdkPlatform?: "web" | "node" | "
|
|
11004
|
+
sdkPlatform?: "web" | "node" | "cli" | "mcp" | "other";
|
|
10986
11005
|
/** 환경 (production/staging/development). 기본 unknown. */
|
|
10987
11006
|
environment?: "production" | "staging" | "development" | "unknown";
|
|
10988
11007
|
}
|
|
@@ -11019,7 +11038,7 @@ interface PlatformIssueDetail {
|
|
|
11019
11038
|
status: "open" | "triaged" | "in_progress" | "resolved" | "wontfix" | "duplicate";
|
|
11020
11039
|
resolution_note?: string;
|
|
11021
11040
|
sdk_version?: string;
|
|
11022
|
-
sdk_platform?: "web" | "node" | "
|
|
11041
|
+
sdk_platform?: "web" | "node" | "cli" | "mcp" | "other";
|
|
11023
11042
|
environment?: "production" | "staging" | "development" | "unknown";
|
|
11024
11043
|
external_links?: Array<{
|
|
11025
11044
|
kind: string;
|
package/dist/index.js
CHANGED
|
@@ -291,7 +291,7 @@ var RealtimeConnectionError = class extends Error {
|
|
|
291
291
|
this.name = "RealtimeConnectionError";
|
|
292
292
|
this.code = init.code || "unknown";
|
|
293
293
|
this.closeCode = init.closeCode;
|
|
294
|
-
this.retryable = init.retryable ?? isRetryableRealtimeRejection(this.code);
|
|
294
|
+
this.retryable = init.retryable ?? isRetryableRealtimeRejection(this.code, init.closeCode);
|
|
295
295
|
this.details = init.details;
|
|
296
296
|
this.retryAfterMs = init.retryAfterMs;
|
|
297
297
|
}
|
|
@@ -308,8 +308,8 @@ var RealtimeConnectionError = class extends Error {
|
|
|
308
308
|
};
|
|
309
309
|
}
|
|
310
310
|
};
|
|
311
|
-
function isRetryableRealtimeRejection(code) {
|
|
312
|
-
return code === "connection_limit_exceeded" || code === "server_at_capacity" || code === "rate_limited" || code === "auth_unavailable" || code === "internal_error" || code === "invalid_ws_ticket" || code === "ws_ticket_unavailable";
|
|
311
|
+
function isRetryableRealtimeRejection(code, closeCode) {
|
|
312
|
+
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";
|
|
313
313
|
}
|
|
314
314
|
var RealtimeRequestError = class extends Error {
|
|
315
315
|
constructor(init) {
|
|
@@ -3807,6 +3807,20 @@ var REALTIME_CLOSING_NOTICE_CODES = /* @__PURE__ */ new Set([
|
|
|
3807
3807
|
]);
|
|
3808
3808
|
var CLOSE_CODE_TOKEN_EXPIRED = 4401;
|
|
3809
3809
|
var CLOSE_CODE_MEMBER_REVOKED = 4403;
|
|
3810
|
+
function toRealtimeRejection(msg) {
|
|
3811
|
+
if (msg.type !== "error" || msg.request_id || typeof msg.close_code !== "number") {
|
|
3812
|
+
return null;
|
|
3813
|
+
}
|
|
3814
|
+
const details = msg.details;
|
|
3815
|
+
return new RealtimeConnectionError({
|
|
3816
|
+
code: typeof msg.code === "string" ? msg.code : void 0,
|
|
3817
|
+
message: typeof msg.message === "string" ? msg.message : void 0,
|
|
3818
|
+
closeCode: msg.close_code,
|
|
3819
|
+
retryable: typeof msg.retryable === "boolean" ? msg.retryable : void 0,
|
|
3820
|
+
details: details && typeof details === "object" && !Array.isArray(details) ? details : void 0,
|
|
3821
|
+
retryAfterMs: typeof msg.retry_after_ms === "number" && msg.retry_after_ms > 0 ? msg.retry_after_ms : void 0
|
|
3822
|
+
});
|
|
3823
|
+
}
|
|
3810
3824
|
var DatabaseAPI = class {
|
|
3811
3825
|
constructor(http) {
|
|
3812
3826
|
this.realtimeWs = null;
|
|
@@ -4749,18 +4763,13 @@ var DatabaseAPI = class {
|
|
|
4749
4763
|
ticket = await this.realtimeTicket();
|
|
4750
4764
|
} catch (e) {
|
|
4751
4765
|
if (e instanceof WsTicketError) {
|
|
4752
|
-
if (e.status === 429 && this.realtimeOptions) {
|
|
4753
|
-
this.attemptRealtimeReconnect();
|
|
4754
|
-
} else {
|
|
4755
|
-
this.setRealtimeState("disconnected");
|
|
4756
|
-
}
|
|
4757
4766
|
const err = new RealtimeConnectionError({
|
|
4758
4767
|
code: e.code,
|
|
4759
4768
|
message: e.message,
|
|
4760
4769
|
closeCode: 4e3 + e.status,
|
|
4761
4770
|
retryAfterMs: e.retryAfterMs
|
|
4762
4771
|
});
|
|
4763
|
-
this.
|
|
4772
|
+
this.handleRealtimeRejection(err);
|
|
4764
4773
|
throw err;
|
|
4765
4774
|
}
|
|
4766
4775
|
throw e;
|
|
@@ -4794,10 +4803,14 @@ var DatabaseAPI = class {
|
|
|
4794
4803
|
this.realtimeOptions.accessToken
|
|
4795
4804
|
);
|
|
4796
4805
|
if (this.realtimeTokenRefresh) url += "&token_refresh=1";
|
|
4806
|
+
url += "&reject_mode=close";
|
|
4797
4807
|
return new Promise((resolve, reject) => {
|
|
4798
4808
|
try {
|
|
4799
|
-
|
|
4809
|
+
const ws = new WebSocket(url);
|
|
4810
|
+
this.realtimeWs = ws;
|
|
4800
4811
|
let settled = false;
|
|
4812
|
+
let accepted = false;
|
|
4813
|
+
let rejection = null;
|
|
4801
4814
|
const connectTimeout = setTimeout(() => {
|
|
4802
4815
|
if (!settled) {
|
|
4803
4816
|
settled = true;
|
|
@@ -4809,7 +4822,8 @@ var DatabaseAPI = class {
|
|
|
4809
4822
|
reject(new Error("Connection timeout"));
|
|
4810
4823
|
}
|
|
4811
4824
|
}, REALTIME_CONNECT_TIMEOUT_MS);
|
|
4812
|
-
|
|
4825
|
+
const accept = () => {
|
|
4826
|
+
accepted = true;
|
|
4813
4827
|
if (!settled) {
|
|
4814
4828
|
settled = true;
|
|
4815
4829
|
clearTimeout(connectTimeout);
|
|
@@ -4822,15 +4836,28 @@ var DatabaseAPI = class {
|
|
|
4822
4836
|
this.resubscribeAll();
|
|
4823
4837
|
resolve();
|
|
4824
4838
|
};
|
|
4825
|
-
|
|
4839
|
+
ws.onopen = () => {
|
|
4840
|
+
ws.send(JSON.stringify({ type: "ping", timestamp: Date.now() }));
|
|
4841
|
+
};
|
|
4842
|
+
ws.onmessage = (event) => {
|
|
4843
|
+
let msg;
|
|
4826
4844
|
try {
|
|
4827
|
-
|
|
4828
|
-
this.handleRealtimeMessage(msg);
|
|
4845
|
+
msg = JSON.parse(event.data);
|
|
4829
4846
|
} catch {
|
|
4830
4847
|
this.debugLog("Failed to parse realtime message");
|
|
4848
|
+
return;
|
|
4849
|
+
}
|
|
4850
|
+
if (!accepted) {
|
|
4851
|
+
const rejected = toRealtimeRejection(msg);
|
|
4852
|
+
if (rejected) {
|
|
4853
|
+
rejection = rejected;
|
|
4854
|
+
return;
|
|
4855
|
+
}
|
|
4856
|
+
accept();
|
|
4831
4857
|
}
|
|
4858
|
+
this.handleRealtimeMessage(msg);
|
|
4832
4859
|
};
|
|
4833
|
-
|
|
4860
|
+
ws.onclose = (event) => {
|
|
4834
4861
|
this.debugLog(
|
|
4835
4862
|
`Database realtime disconnected (code=${event?.code}, reason=${event?.reason})`
|
|
4836
4863
|
);
|
|
@@ -4839,12 +4866,25 @@ var DatabaseAPI = class {
|
|
|
4839
4866
|
this.clearRealtimeAuthRetry();
|
|
4840
4867
|
const notice = this.realtimeCloseNotice;
|
|
4841
4868
|
this.realtimeCloseNotice = null;
|
|
4869
|
+
const code = event?.code;
|
|
4870
|
+
if (!accepted && code >= 4e3 && code <= 4999) {
|
|
4871
|
+
const err = rejection ?? new RealtimeConnectionError({
|
|
4872
|
+
code: event.reason || void 0,
|
|
4873
|
+
closeCode: code
|
|
4874
|
+
});
|
|
4875
|
+
if (!settled) {
|
|
4876
|
+
settled = true;
|
|
4877
|
+
clearTimeout(connectTimeout);
|
|
4878
|
+
reject(err);
|
|
4879
|
+
}
|
|
4880
|
+
this.handleRealtimeRejection(err);
|
|
4881
|
+
return;
|
|
4882
|
+
}
|
|
4842
4883
|
if (!settled) {
|
|
4843
4884
|
settled = true;
|
|
4844
4885
|
clearTimeout(connectTimeout);
|
|
4845
4886
|
reject(new Error("Connection closed during handshake"));
|
|
4846
4887
|
}
|
|
4847
|
-
const code = event?.code;
|
|
4848
4888
|
if (this.realtimeOptions && this.realtimeState !== "disconnected" && (code === CLOSE_CODE_MEMBER_REVOKED || code === CLOSE_CODE_TOKEN_EXPIRED && !this.http.canRefreshMemberToken(
|
|
4849
4889
|
this.realtimeOptions.accessToken
|
|
4850
4890
|
))) {
|
|
@@ -4862,7 +4902,7 @@ var DatabaseAPI = class {
|
|
|
4862
4902
|
this.attemptRealtimeReconnect();
|
|
4863
4903
|
}
|
|
4864
4904
|
};
|
|
4865
|
-
|
|
4905
|
+
ws.onerror = () => {
|
|
4866
4906
|
this.debugLog("Database realtime error");
|
|
4867
4907
|
this.realtimeOnError?.(new Error("WebSocket connection error"));
|
|
4868
4908
|
};
|
|
@@ -4872,6 +4912,21 @@ var DatabaseAPI = class {
|
|
|
4872
4912
|
}
|
|
4873
4913
|
});
|
|
4874
4914
|
}
|
|
4915
|
+
/**
|
|
4916
|
+
* 서버가 연결을 거부했다(티켓 발급 거부, 또는 reject_mode=close 로 받은 업그레이드 거부). cb.realtime 과 같은 규칙이다 —
|
|
4917
|
+
* 다시 해서 될 거부(`retryable`: 일시 장애, 속도 제한, 동시접속 한도, 만료된 티켓)는 서버가 알려 준 시각(`retryAfterMs`)
|
|
4918
|
+
* 이후로 재연결을 이어 가고, 아니면(자격증명, 멤버 폐기, 할당량) 멈추고 `onRealtimeError` 로 알린다. 재시도를 다 써도
|
|
4919
|
+
* 거부되면 마지막 거부를 알린다.
|
|
4920
|
+
*/
|
|
4921
|
+
handleRealtimeRejection(err) {
|
|
4922
|
+
if (!this.realtimeOptions || this.realtimeState === "disconnected") return;
|
|
4923
|
+
if (err.retryable) {
|
|
4924
|
+
this.attemptRealtimeReconnect(err);
|
|
4925
|
+
return;
|
|
4926
|
+
}
|
|
4927
|
+
this.setRealtimeState("disconnected");
|
|
4928
|
+
this.realtimeOnError?.(err);
|
|
4929
|
+
}
|
|
4875
4930
|
/**
|
|
4876
4931
|
* 서버에 subscribe 메시지를 보내고 서버 subscription_id를 반환하는 Promise
|
|
4877
4932
|
*/
|
|
@@ -5044,21 +5099,25 @@ var DatabaseAPI = class {
|
|
|
5044
5099
|
break;
|
|
5045
5100
|
}
|
|
5046
5101
|
}
|
|
5047
|
-
|
|
5102
|
+
/**
|
|
5103
|
+
* 지수 백오프로 다시 붙는다. cause 는 이번 시도를 끝낸 서버의 거부다 — 알려 준 재시도 시각(`retryAfterMs`)보다 일찍 다시
|
|
5104
|
+
* 하지 않고, 재시도를 다 쓰면 일반 오류 대신 그 거부를 알린다.
|
|
5105
|
+
*/
|
|
5106
|
+
attemptRealtimeReconnect(cause) {
|
|
5048
5107
|
const maxRetries = this.realtimeOptions?.maxRetries ?? 5;
|
|
5049
5108
|
const retryInterval = this.realtimeOptions?.retryInterval ?? 1e3;
|
|
5050
5109
|
if (this.realtimeRetryCount >= maxRetries) {
|
|
5051
5110
|
this.setRealtimeState("disconnected");
|
|
5052
5111
|
this.realtimeOnError?.(
|
|
5053
|
-
new Error("Realtime connection lost. Max retries exceeded.")
|
|
5112
|
+
cause ?? new Error("Realtime connection lost. Max retries exceeded.")
|
|
5054
5113
|
);
|
|
5055
5114
|
return;
|
|
5056
5115
|
}
|
|
5057
5116
|
this.setRealtimeState("connecting");
|
|
5058
5117
|
this.realtimeRetryCount++;
|
|
5059
|
-
const delay = Math.
|
|
5060
|
-
retryInterval * 2 ** (this.realtimeRetryCount - 1),
|
|
5061
|
-
|
|
5118
|
+
const delay = Math.max(
|
|
5119
|
+
Math.min(retryInterval * 2 ** (this.realtimeRetryCount - 1), 3e4),
|
|
5120
|
+
cause?.retryAfterMs ?? 0
|
|
5062
5121
|
);
|
|
5063
5122
|
this.debugLog(
|
|
5064
5123
|
`Reconnecting in ${delay}ms (attempt ${this.realtimeRetryCount}/${maxRetries})`
|
|
@@ -13550,8 +13609,8 @@ var SupportAPI = class {
|
|
|
13550
13609
|
}
|
|
13551
13610
|
};
|
|
13552
13611
|
function detectSdkVersion() {
|
|
13553
|
-
if ("6.
|
|
13554
|
-
return "6.
|
|
13612
|
+
if ("6.15.0") {
|
|
13613
|
+
return "6.15.0";
|
|
13555
13614
|
}
|
|
13556
13615
|
return "unknown";
|
|
13557
13616
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -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" || code === "invalid_ws_ticket" || code === "ws_ticket_unavailable";
|
|
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) {
|
|
@@ -3754,6 +3754,20 @@ var REALTIME_CLOSING_NOTICE_CODES = /* @__PURE__ */ new Set([
|
|
|
3754
3754
|
]);
|
|
3755
3755
|
var CLOSE_CODE_TOKEN_EXPIRED = 4401;
|
|
3756
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
|
+
}
|
|
3757
3771
|
var DatabaseAPI = class {
|
|
3758
3772
|
constructor(http) {
|
|
3759
3773
|
this.realtimeWs = null;
|
|
@@ -4696,18 +4710,13 @@ var DatabaseAPI = class {
|
|
|
4696
4710
|
ticket = await this.realtimeTicket();
|
|
4697
4711
|
} catch (e) {
|
|
4698
4712
|
if (e instanceof WsTicketError) {
|
|
4699
|
-
if (e.status === 429 && this.realtimeOptions) {
|
|
4700
|
-
this.attemptRealtimeReconnect();
|
|
4701
|
-
} else {
|
|
4702
|
-
this.setRealtimeState("disconnected");
|
|
4703
|
-
}
|
|
4704
4713
|
const err = new RealtimeConnectionError({
|
|
4705
4714
|
code: e.code,
|
|
4706
4715
|
message: e.message,
|
|
4707
4716
|
closeCode: 4e3 + e.status,
|
|
4708
4717
|
retryAfterMs: e.retryAfterMs
|
|
4709
4718
|
});
|
|
4710
|
-
this.
|
|
4719
|
+
this.handleRealtimeRejection(err);
|
|
4711
4720
|
throw err;
|
|
4712
4721
|
}
|
|
4713
4722
|
throw e;
|
|
@@ -4741,10 +4750,14 @@ var DatabaseAPI = class {
|
|
|
4741
4750
|
this.realtimeOptions.accessToken
|
|
4742
4751
|
);
|
|
4743
4752
|
if (this.realtimeTokenRefresh) url += "&token_refresh=1";
|
|
4753
|
+
url += "&reject_mode=close";
|
|
4744
4754
|
return new Promise((resolve, reject) => {
|
|
4745
4755
|
try {
|
|
4746
|
-
|
|
4756
|
+
const ws = new WebSocket(url);
|
|
4757
|
+
this.realtimeWs = ws;
|
|
4747
4758
|
let settled = false;
|
|
4759
|
+
let accepted = false;
|
|
4760
|
+
let rejection = null;
|
|
4748
4761
|
const connectTimeout = setTimeout(() => {
|
|
4749
4762
|
if (!settled) {
|
|
4750
4763
|
settled = true;
|
|
@@ -4756,7 +4769,8 @@ var DatabaseAPI = class {
|
|
|
4756
4769
|
reject(new Error("Connection timeout"));
|
|
4757
4770
|
}
|
|
4758
4771
|
}, REALTIME_CONNECT_TIMEOUT_MS);
|
|
4759
|
-
|
|
4772
|
+
const accept = () => {
|
|
4773
|
+
accepted = true;
|
|
4760
4774
|
if (!settled) {
|
|
4761
4775
|
settled = true;
|
|
4762
4776
|
clearTimeout(connectTimeout);
|
|
@@ -4769,15 +4783,28 @@ var DatabaseAPI = class {
|
|
|
4769
4783
|
this.resubscribeAll();
|
|
4770
4784
|
resolve();
|
|
4771
4785
|
};
|
|
4772
|
-
|
|
4786
|
+
ws.onopen = () => {
|
|
4787
|
+
ws.send(JSON.stringify({ type: "ping", timestamp: Date.now() }));
|
|
4788
|
+
};
|
|
4789
|
+
ws.onmessage = (event) => {
|
|
4790
|
+
let msg;
|
|
4773
4791
|
try {
|
|
4774
|
-
|
|
4775
|
-
this.handleRealtimeMessage(msg);
|
|
4792
|
+
msg = JSON.parse(event.data);
|
|
4776
4793
|
} catch {
|
|
4777
4794
|
this.debugLog("Failed to parse realtime message");
|
|
4795
|
+
return;
|
|
4796
|
+
}
|
|
4797
|
+
if (!accepted) {
|
|
4798
|
+
const rejected = toRealtimeRejection(msg);
|
|
4799
|
+
if (rejected) {
|
|
4800
|
+
rejection = rejected;
|
|
4801
|
+
return;
|
|
4802
|
+
}
|
|
4803
|
+
accept();
|
|
4778
4804
|
}
|
|
4805
|
+
this.handleRealtimeMessage(msg);
|
|
4779
4806
|
};
|
|
4780
|
-
|
|
4807
|
+
ws.onclose = (event) => {
|
|
4781
4808
|
this.debugLog(
|
|
4782
4809
|
`Database realtime disconnected (code=${event?.code}, reason=${event?.reason})`
|
|
4783
4810
|
);
|
|
@@ -4786,12 +4813,25 @@ var DatabaseAPI = class {
|
|
|
4786
4813
|
this.clearRealtimeAuthRetry();
|
|
4787
4814
|
const notice = this.realtimeCloseNotice;
|
|
4788
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
|
+
}
|
|
4789
4830
|
if (!settled) {
|
|
4790
4831
|
settled = true;
|
|
4791
4832
|
clearTimeout(connectTimeout);
|
|
4792
4833
|
reject(new Error("Connection closed during handshake"));
|
|
4793
4834
|
}
|
|
4794
|
-
const code = event?.code;
|
|
4795
4835
|
if (this.realtimeOptions && this.realtimeState !== "disconnected" && (code === CLOSE_CODE_MEMBER_REVOKED || code === CLOSE_CODE_TOKEN_EXPIRED && !this.http.canRefreshMemberToken(
|
|
4796
4836
|
this.realtimeOptions.accessToken
|
|
4797
4837
|
))) {
|
|
@@ -4809,7 +4849,7 @@ var DatabaseAPI = class {
|
|
|
4809
4849
|
this.attemptRealtimeReconnect();
|
|
4810
4850
|
}
|
|
4811
4851
|
};
|
|
4812
|
-
|
|
4852
|
+
ws.onerror = () => {
|
|
4813
4853
|
this.debugLog("Database realtime error");
|
|
4814
4854
|
this.realtimeOnError?.(new Error("WebSocket connection error"));
|
|
4815
4855
|
};
|
|
@@ -4819,6 +4859,21 @@ var DatabaseAPI = class {
|
|
|
4819
4859
|
}
|
|
4820
4860
|
});
|
|
4821
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
|
+
}
|
|
4822
4877
|
/**
|
|
4823
4878
|
* 서버에 subscribe 메시지를 보내고 서버 subscription_id를 반환하는 Promise
|
|
4824
4879
|
*/
|
|
@@ -4991,21 +5046,25 @@ var DatabaseAPI = class {
|
|
|
4991
5046
|
break;
|
|
4992
5047
|
}
|
|
4993
5048
|
}
|
|
4994
|
-
|
|
5049
|
+
/**
|
|
5050
|
+
* 지수 백오프로 다시 붙는다. cause 는 이번 시도를 끝낸 서버의 거부다 — 알려 준 재시도 시각(`retryAfterMs`)보다 일찍 다시
|
|
5051
|
+
* 하지 않고, 재시도를 다 쓰면 일반 오류 대신 그 거부를 알린다.
|
|
5052
|
+
*/
|
|
5053
|
+
attemptRealtimeReconnect(cause) {
|
|
4995
5054
|
const maxRetries = this.realtimeOptions?.maxRetries ?? 5;
|
|
4996
5055
|
const retryInterval = this.realtimeOptions?.retryInterval ?? 1e3;
|
|
4997
5056
|
if (this.realtimeRetryCount >= maxRetries) {
|
|
4998
5057
|
this.setRealtimeState("disconnected");
|
|
4999
5058
|
this.realtimeOnError?.(
|
|
5000
|
-
new Error("Realtime connection lost. Max retries exceeded.")
|
|
5059
|
+
cause ?? new Error("Realtime connection lost. Max retries exceeded.")
|
|
5001
5060
|
);
|
|
5002
5061
|
return;
|
|
5003
5062
|
}
|
|
5004
5063
|
this.setRealtimeState("connecting");
|
|
5005
5064
|
this.realtimeRetryCount++;
|
|
5006
|
-
const delay = Math.
|
|
5007
|
-
retryInterval * 2 ** (this.realtimeRetryCount - 1),
|
|
5008
|
-
|
|
5065
|
+
const delay = Math.max(
|
|
5066
|
+
Math.min(retryInterval * 2 ** (this.realtimeRetryCount - 1), 3e4),
|
|
5067
|
+
cause?.retryAfterMs ?? 0
|
|
5009
5068
|
);
|
|
5010
5069
|
this.debugLog(
|
|
5011
5070
|
`Reconnecting in ${delay}ms (attempt ${this.realtimeRetryCount}/${maxRetries})`
|
|
@@ -13497,8 +13556,8 @@ var SupportAPI = class {
|
|
|
13497
13556
|
}
|
|
13498
13557
|
};
|
|
13499
13558
|
function detectSdkVersion() {
|
|
13500
|
-
if ("6.
|
|
13501
|
-
return "6.
|
|
13559
|
+
if ("6.15.0") {
|
|
13560
|
+
return "6.15.0";
|
|
13502
13561
|
}
|
|
13503
13562
|
return "unknown";
|
|
13504
13563
|
}
|