connectbase-client 6.4.0 → 6.5.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.mjs CHANGED
@@ -232,6 +232,30 @@ var GameError = class extends Error {
232
232
  };
233
233
  }
234
234
  };
235
+ var RealtimeConnectionError = class extends Error {
236
+ constructor(init) {
237
+ super(init.message || init.code || "Realtime connection rejected");
238
+ this.name = "RealtimeConnectionError";
239
+ this.code = init.code || "unknown";
240
+ this.closeCode = init.closeCode;
241
+ this.retryable = init.retryable ?? isRetryableRealtimeRejection(this.code);
242
+ this.details = init.details;
243
+ }
244
+ /** `JSON.stringify(err)` 가 `{}` 가 되지 않도록 한다 (ApiError.toJSON 참조). */
245
+ toJSON() {
246
+ return {
247
+ name: this.name,
248
+ code: this.code,
249
+ message: this.message,
250
+ closeCode: this.closeCode,
251
+ retryable: this.retryable,
252
+ details: this.details
253
+ };
254
+ }
255
+ };
256
+ function isRetryableRealtimeRejection(code) {
257
+ return code === "connection_limit_exceeded" || code === "server_at_capacity" || code === "auth_unavailable" || code === "internal_error";
258
+ }
235
259
 
236
260
  // src/api/ai.ts
237
261
  function aiErrorToApiError(err) {
@@ -7874,6 +7898,18 @@ var QueueAPI = class {
7874
7898
 
7875
7899
  // src/api/realtime.ts
7876
7900
  var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
7901
+ function toConnectionRejection(msg) {
7902
+ if (msg.event !== "error" || msg.request_id) return null;
7903
+ const data = msg.data;
7904
+ if (!data || typeof data.close_code !== "number") return null;
7905
+ return new RealtimeConnectionError({
7906
+ code: data.code,
7907
+ message: data.message || msg.error,
7908
+ closeCode: data.close_code,
7909
+ retryable: data.retryable,
7910
+ details: data.details
7911
+ });
7912
+ }
7877
7913
  var RealtimeAPI = class {
7878
7914
  constructor(http, socketUrl) {
7879
7915
  this.ws = null;
@@ -7901,6 +7937,16 @@ var RealtimeAPI = class {
7901
7937
  fallback: "sse"
7902
7938
  };
7903
7939
  this.retryCount = 0;
7940
+ /**
7941
+ * 재연결 뒤 복구할 구독 상태. 연결이 끊긴 첫 순간에 떠 두고, 재연결이 성공해 복구한 뒤 비운다.
7942
+ *
7943
+ * 재연결 시도가 실패할 때마다 onclose → handleDisconnect 가 다시 불린다. 예전에는 그때마다 구독 맵을
7944
+ * 새로 떴는데, 첫 끊김에서 이미 비워진 맵이라 빈 스냅샷으로 덮였다 — 첫 재연결 시도가 실패하면
7945
+ * (서버 롤링 재시작 등) 재연결에 성공해도 구독이 하나도 복구되지 않았다.
7946
+ */
7947
+ this.pendingRestore = null;
7948
+ /** 첫 연결 전에 direct 원본 힌트를 한 번 물었는지 (primeDirectRealtime). */
7949
+ this.directRealtimePrimed = false;
7904
7950
  this.pendingRequests = /* @__PURE__ */ new Map();
7905
7951
  this.subscriptions = /* @__PURE__ */ new Map();
7906
7952
  this.streamSessions = /* @__PURE__ */ new Map();
@@ -7944,7 +7990,7 @@ var RealtimeAPI = class {
7944
7990
  if (this.state === "connected") {
7945
7991
  return;
7946
7992
  }
7947
- if (this.state === "connecting" && this.connectPromise) {
7993
+ if (this.connectPromise) {
7948
7994
  return this.connectPromise;
7949
7995
  }
7950
7996
  this.options = { ...this.options, ...options };
@@ -7956,10 +8002,15 @@ var RealtimeAPI = class {
7956
8002
  `Invalid userId: "${this.userId}" looks like a UUID, which is reserved for authenticated members. Pass that member's accessToken (JWT) instead of userId+publicKey, or use a non-UUID identifier for anonymous/guest connections.`
7957
8003
  );
7958
8004
  }
7959
- this.connectPromise = this.doConnect();
8005
+ this.connectPromise = this.primeDirectRealtime().then(
8006
+ () => this.doConnect()
8007
+ );
7960
8008
  try {
7961
8009
  await this.connectPromise;
7962
8010
  } catch (e) {
8011
+ if (e instanceof RealtimeConnectionError) {
8012
+ throw e;
8013
+ }
7963
8014
  const hasAuth = !!(this.options.accessToken || this.http.getPublicKey());
7964
8015
  if (this.options.fallback !== "none" && hasAuth) {
7965
8016
  this.activateSseFallback(e);
@@ -7988,6 +8039,7 @@ var RealtimeAPI = class {
7988
8039
  this.activeTransport = "sse";
7989
8040
  this.state = "connected";
7990
8041
  this.retryCount = 0;
8042
+ this.pendingRestore = null;
7991
8043
  this.log(
7992
8044
  `WS unavailable (${causeMsg}); using SSE fallback transport for AI streaming`
7993
8045
  );
@@ -8044,6 +8096,7 @@ var RealtimeAPI = class {
8044
8096
  this._appId = null;
8045
8097
  this.retryCount = 0;
8046
8098
  this.connectPromise = null;
8099
+ this.pendingRestore = null;
8047
8100
  }
8048
8101
  /**
8049
8102
  * 카테고리 구독
@@ -8826,6 +8879,30 @@ var RealtimeAPI = class {
8826
8879
  };
8827
8880
  }
8828
8881
  // Private methods
8882
+ /**
8883
+ * direct(저지연) 원본을 아직 모르면 첫 연결 전에 한 번 묻는다 — `GET /v1/public/edge-hints`.
8884
+ *
8885
+ * direct 원본은 core-server 응답 헤더(`X-CB-Realtime-Direct-Base`)로만 배운다. realtime 만 쓰는
8886
+ * 페이지는 WS 를 열기 전에 REST 를 부르지 않아 배울 기회가 없었고, 첫 연결이 늘 Cloudflare 를 타
8887
+ * 한국 사용자는 미국(LAX)을 거친 ~270ms 로 붙었다(platform-issue 01a15d9d 의 273ms). 응답 헤더는
8888
+ * HttpClient 가 평소처럼 학습한다. 광고 대상이 아니면(해외, 킬 스위치) 빈 응답이라 원래 소켓
8889
+ * URL 로 간다. 실패하거나 느리면(1.5초) 그냥 넘어간다 — 최적화일 뿐 연결의 전제가 아니다.
8890
+ * 세션당 한 번만 묻는다.
8891
+ */
8892
+ async primeDirectRealtime() {
8893
+ if (this.directRealtimePrimed) return;
8894
+ this.directRealtimePrimed = true;
8895
+ if (this.http.getDirectRealtimeBaseUrl() || !this.http.getPublicKey()) {
8896
+ return;
8897
+ }
8898
+ try {
8899
+ await this.http.get("/v1/public/edge-hints", { timeout: 1500 });
8900
+ } catch (e) {
8901
+ this.log(
8902
+ `edge hint lookup failed, using default socket URL: ${e instanceof Error ? e.message : String(e)}`
8903
+ );
8904
+ }
8905
+ }
8829
8906
  async doConnect() {
8830
8907
  return new Promise((resolve, reject) => {
8831
8908
  this.state = "connecting";
@@ -8854,7 +8931,9 @@ var RealtimeAPI = class {
8854
8931
  if (this.userId) {
8855
8932
  url += `&user_id=${encodeURIComponent(this.userId)}`;
8856
8933
  }
8934
+ url += "&reject_mode=close";
8857
8935
  let settled = false;
8936
+ let rejection = null;
8858
8937
  const timeoutId = setTimeout(() => {
8859
8938
  if (!settled) {
8860
8939
  settled = true;
@@ -8882,6 +8961,11 @@ var RealtimeAPI = class {
8882
8961
  for (const line of messages) {
8883
8962
  try {
8884
8963
  const msg = JSON.parse(line);
8964
+ const rejected = toConnectionRejection(msg);
8965
+ if (rejected && !settled) {
8966
+ rejection = rejected;
8967
+ continue;
8968
+ }
8885
8969
  this.handleServerMessage(msg, () => {
8886
8970
  if (!settled) {
8887
8971
  settled = true;
@@ -8899,6 +8983,30 @@ var RealtimeAPI = class {
8899
8983
  this.log(
8900
8984
  `WebSocket closed: code=${event.code}, reason=${event.reason}`
8901
8985
  );
8986
+ if (event.code >= 4e3 && event.code <= 4999) {
8987
+ const err = rejection ?? new RealtimeConnectionError({
8988
+ code: event.reason || void 0,
8989
+ closeCode: event.code
8990
+ });
8991
+ const wasConnected = this.state === "connected";
8992
+ if (!settled) {
8993
+ settled = true;
8994
+ clearTimeout(timeoutId);
8995
+ }
8996
+ this.ws = null;
8997
+ if (wasConnected) {
8998
+ if (err.retryable) {
8999
+ this.handleDisconnect();
9000
+ } else {
9001
+ this.failRealtime(err);
9002
+ }
9003
+ return;
9004
+ }
9005
+ this.state = "disconnected";
9006
+ this.notifyStateChange();
9007
+ reject(err);
9008
+ return;
9009
+ }
8902
9010
  if (!settled && this.state === "connecting") {
8903
9011
  settled = true;
8904
9012
  clearTimeout(timeoutId);
@@ -9145,20 +9253,23 @@ var RealtimeAPI = class {
9145
9253
  }
9146
9254
  });
9147
9255
  this.streamSessions.clear();
9148
- const previousSubscriptions = /* @__PURE__ */ new Map();
9149
- for (const [category, sub] of this.subscriptions) {
9150
- previousSubscriptions.set(category, [...sub.handlers]);
9256
+ if (!this.pendingRestore) {
9257
+ const subscriptions = /* @__PURE__ */ new Map();
9258
+ for (const [category, sub] of this.subscriptions) {
9259
+ subscriptions.set(category, [...sub.handlers]);
9260
+ }
9261
+ const presence = /* @__PURE__ */ new Map();
9262
+ for (const [userId, handlers] of this.presenceSubscriptions) {
9263
+ presence.set(userId, [...handlers]);
9264
+ }
9265
+ const typing = /* @__PURE__ */ new Map();
9266
+ for (const [roomId, handlers] of this.typingHandlers) {
9267
+ typing.set(roomId, [...handlers]);
9268
+ }
9269
+ this.pendingRestore = { subscriptions, presence, typing };
9151
9270
  }
9152
9271
  this.subscriptions.clear();
9153
- const previousPresenceSubscriptions = /* @__PURE__ */ new Map();
9154
- for (const [userId, handlers] of this.presenceSubscriptions) {
9155
- previousPresenceSubscriptions.set(userId, [...handlers]);
9156
- }
9157
9272
  this.presenceSubscriptions.clear();
9158
- const previousTypingHandlers = /* @__PURE__ */ new Map();
9159
- for (const [roomId, handlers] of this.typingHandlers) {
9160
- previousTypingHandlers.set(roomId, [...handlers]);
9161
- }
9162
9273
  this.typingHandlers.clear();
9163
9274
  if (this.retryCount < this.options.maxRetries) {
9164
9275
  this.state = "reconnecting";
@@ -9170,16 +9281,29 @@ var RealtimeAPI = class {
9170
9281
  );
9171
9282
  setTimeout(async () => {
9172
9283
  if (this.sseFallbackActive) return;
9284
+ if (this.state !== "reconnecting") return;
9173
9285
  try {
9174
9286
  await this.doConnect();
9175
9287
  this.log("Reconnected successfully, restoring subscriptions...");
9176
- await this.restoreSubscriptions(
9177
- previousSubscriptions,
9178
- previousPresenceSubscriptions,
9179
- previousTypingHandlers
9180
- );
9288
+ const snapshot = this.pendingRestore;
9289
+ this.pendingRestore = null;
9290
+ if (snapshot) {
9291
+ await this.restoreSubscriptions(
9292
+ snapshot.subscriptions,
9293
+ snapshot.presence,
9294
+ snapshot.typing
9295
+ );
9296
+ }
9181
9297
  } catch (e) {
9182
9298
  this.logError("Reconnect failed", e);
9299
+ if (e instanceof RealtimeConnectionError) {
9300
+ if (e.retryable && this.retryCount < this.options.maxRetries) {
9301
+ this.handleDisconnect();
9302
+ } else {
9303
+ this.failRealtime(e);
9304
+ }
9305
+ return;
9306
+ }
9183
9307
  if (this.retryCount >= this.options.maxRetries && this.options.fallback !== "none" && !this.sseFallbackActive) {
9184
9308
  this.activateSseFallback(e);
9185
9309
  }
@@ -9197,6 +9321,18 @@ var RealtimeAPI = class {
9197
9321
  }
9198
9322
  }
9199
9323
  }
9324
+ /**
9325
+ * 재연결을 멈추고 앱에 알린다 — 서버가 재시도해도 소용없는 이유(자격증명, 할당량)로 거부했거나
9326
+ * 재시도할 수 있는 거부가 maxRetries 를 다 썼다.
9327
+ */
9328
+ failRealtime(err) {
9329
+ this.pendingRestore = null;
9330
+ this.ws = null;
9331
+ this._connectionId = null;
9332
+ this.state = "disconnected";
9333
+ this.notifyStateChange();
9334
+ this.notifyError(err);
9335
+ }
9200
9336
  /**
9201
9337
  * 이전 구독 상태를 복구합니다.
9202
9338
  * 재연결 성공 후 호출됩니다.
@@ -14173,6 +14309,7 @@ export {
14173
14309
  GameRoomTransport,
14174
14310
  NativeAPI,
14175
14311
  OrganizationsAPI,
14312
+ RealtimeConnectionError,
14176
14313
  RolesAPI,
14177
14314
  SessionManager,
14178
14315
  VideoProcessingError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "connectbase-client",
3
- "version": "6.4.0",
3
+ "version": "6.5.0",
4
4
  "description": "Connect Base JavaScript/TypeScript SDK for browser and Node.js",
5
5
  "repository": {
6
6
  "type": "git",