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/CHANGELOG.md +50 -0
- package/README.md +23 -0
- package/dist/cli.js +1 -1
- package/dist/connect-base.umd.js +5 -5
- package/dist/index.d.mts +85 -1
- package/dist/index.d.ts +85 -1
- package/dist/index.js +156 -18
- package/dist/index.mjs +155 -18
- package/package.json +1 -1
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.
|
|
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.
|
|
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
|
-
|
|
9149
|
-
|
|
9150
|
-
|
|
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
|
-
|
|
9177
|
-
|
|
9178
|
-
|
|
9179
|
-
|
|
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,
|