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/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" | "unity" | "godot" | "unreal" | "cli" | "mcp" | "other";
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" | "unity" | "godot" | "unreal" | "cli" | "mcp" | "other";
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" | "unity" | "godot" | "unreal" | "cli" | "mcp" | "other";
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" | "unity" | "godot" | "unreal" | "cli" | "mcp" | "other";
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.realtimeOnError?.(err);
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
- this.realtimeWs = new WebSocket(url);
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
- this.realtimeWs.onopen = () => {
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
- this.realtimeWs.onmessage = (event) => {
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
- const msg = JSON.parse(event.data);
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
- this.realtimeWs.onclose = (event) => {
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
- this.realtimeWs.onerror = () => {
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
- attemptRealtimeReconnect() {
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.min(
5060
- retryInterval * 2 ** (this.realtimeRetryCount - 1),
5061
- 3e4
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.14.0") {
13554
- return "6.14.0";
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.realtimeOnError?.(err);
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
- this.realtimeWs = new WebSocket(url);
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
- this.realtimeWs.onopen = () => {
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
- this.realtimeWs.onmessage = (event) => {
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
- const msg = JSON.parse(event.data);
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
- this.realtimeWs.onclose = (event) => {
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
- this.realtimeWs.onerror = () => {
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
- attemptRealtimeReconnect() {
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.min(
5007
- retryInterval * 2 ** (this.realtimeRetryCount - 1),
5008
- 3e4
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.14.0") {
13501
- return "6.14.0";
13559
+ if ("6.15.0") {
13560
+ return "6.15.0";
13502
13561
  }
13503
13562
  return "unknown";
13504
13563
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "connectbase-client",
3
- "version": "6.14.0",
3
+ "version": "6.15.0",
4
4
  "description": "Connect Base JavaScript/TypeScript SDK for browser and Node.js",
5
5
  "repository": {
6
6
  "type": "git",