connectbase-client 6.5.0 → 6.6.1
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 +70 -0
- package/README.md +39 -0
- package/dist/cli.js +1 -1
- package/dist/connect-base.umd.js +5 -5
- package/dist/index.d.mts +116 -2
- package/dist/index.d.ts +116 -2
- package/dist/index.js +263 -64
- package/dist/index.mjs +262 -64
- package/package.json +14 -7
package/dist/index.js
CHANGED
|
@@ -37,6 +37,7 @@ __export(index_exports, {
|
|
|
37
37
|
NativeAPI: () => NativeAPI,
|
|
38
38
|
OrganizationsAPI: () => OrganizationsAPI,
|
|
39
39
|
RealtimeConnectionError: () => RealtimeConnectionError,
|
|
40
|
+
RealtimeRequestError: () => RealtimeRequestError,
|
|
40
41
|
RolesAPI: () => RolesAPI,
|
|
41
42
|
SessionManager: () => SessionManager,
|
|
42
43
|
VideoProcessingError: () => VideoProcessingError,
|
|
@@ -291,6 +292,7 @@ var RealtimeConnectionError = class extends Error {
|
|
|
291
292
|
this.closeCode = init.closeCode;
|
|
292
293
|
this.retryable = init.retryable ?? isRetryableRealtimeRejection(this.code);
|
|
293
294
|
this.details = init.details;
|
|
295
|
+
this.retryAfterMs = init.retryAfterMs;
|
|
294
296
|
}
|
|
295
297
|
/** `JSON.stringify(err)` 가 `{}` 가 되지 않도록 한다 (ApiError.toJSON 참조). */
|
|
296
298
|
toJSON() {
|
|
@@ -300,13 +302,31 @@ var RealtimeConnectionError = class extends Error {
|
|
|
300
302
|
message: this.message,
|
|
301
303
|
closeCode: this.closeCode,
|
|
302
304
|
retryable: this.retryable,
|
|
303
|
-
details: this.details
|
|
305
|
+
details: this.details,
|
|
306
|
+
retryAfterMs: this.retryAfterMs
|
|
304
307
|
};
|
|
305
308
|
}
|
|
306
309
|
};
|
|
307
310
|
function isRetryableRealtimeRejection(code) {
|
|
308
|
-
return code === "connection_limit_exceeded" || code === "server_at_capacity" || code === "auth_unavailable" || code === "internal_error";
|
|
311
|
+
return code === "connection_limit_exceeded" || code === "server_at_capacity" || code === "rate_limited" || code === "auth_unavailable" || code === "internal_error";
|
|
309
312
|
}
|
|
313
|
+
var RealtimeRequestError = class extends Error {
|
|
314
|
+
constructor(init) {
|
|
315
|
+
super(init.message || init.code || "Realtime request failed");
|
|
316
|
+
this.name = "RealtimeRequestError";
|
|
317
|
+
this.code = init.code || "unknown";
|
|
318
|
+
this.category = init.category;
|
|
319
|
+
}
|
|
320
|
+
/** `JSON.stringify(err)` 가 `{}` 가 되지 않도록 한다 (ApiError.toJSON 참조). */
|
|
321
|
+
toJSON() {
|
|
322
|
+
return {
|
|
323
|
+
name: this.name,
|
|
324
|
+
code: this.code,
|
|
325
|
+
message: this.message,
|
|
326
|
+
category: this.category
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
};
|
|
310
330
|
|
|
311
331
|
// src/api/ai.ts
|
|
312
332
|
function aiErrorToApiError(err) {
|
|
@@ -7958,9 +7978,35 @@ function toConnectionRejection(msg) {
|
|
|
7958
7978
|
message: data.message || msg.error,
|
|
7959
7979
|
closeCode: data.close_code,
|
|
7960
7980
|
retryable: data.retryable,
|
|
7961
|
-
details: data.details
|
|
7981
|
+
details: data.details,
|
|
7982
|
+
retryAfterMs: typeof data.retry_after_ms === "number" && data.retry_after_ms > 0 ? data.retry_after_ms : void 0
|
|
7962
7983
|
});
|
|
7963
7984
|
}
|
|
7985
|
+
var HandshakeFailedError = class extends Error {
|
|
7986
|
+
};
|
|
7987
|
+
var ConnectCancelledError = class extends Error {
|
|
7988
|
+
constructor() {
|
|
7989
|
+
super("Realtime connect cancelled by disconnect()");
|
|
7990
|
+
this.name = "AbortError";
|
|
7991
|
+
}
|
|
7992
|
+
};
|
|
7993
|
+
var MAX_HANDSHAKE_RETRIES = 2;
|
|
7994
|
+
function jitter(max) {
|
|
7995
|
+
return Math.floor(Math.random() * Math.max(0, max));
|
|
7996
|
+
}
|
|
7997
|
+
function toSendPolicy(value) {
|
|
7998
|
+
return value === "upstream" || value === "publishers_only" ? value : "open";
|
|
7999
|
+
}
|
|
8000
|
+
function toCategoryInfo(response) {
|
|
8001
|
+
return {
|
|
8002
|
+
category: response.category,
|
|
8003
|
+
persist: response.persist,
|
|
8004
|
+
historyCount: response.history_count,
|
|
8005
|
+
readReceipt: response.read_receipt,
|
|
8006
|
+
isPublisher: response.publisher === true,
|
|
8007
|
+
sendPolicy: toSendPolicy(response.send_policy)
|
|
8008
|
+
};
|
|
8009
|
+
}
|
|
7964
8010
|
var RealtimeAPI = class {
|
|
7965
8011
|
constructor(http, socketUrl) {
|
|
7966
8012
|
this.ws = null;
|
|
@@ -7998,6 +8044,18 @@ var RealtimeAPI = class {
|
|
|
7998
8044
|
this.pendingRestore = null;
|
|
7999
8045
|
/** 첫 연결 전에 direct 원본 힌트를 한 번 물었는지 (primeDirectRealtime). */
|
|
8000
8046
|
this.directRealtimePrimed = false;
|
|
8047
|
+
/** disconnect() 마다 늘린다 — connect() 의 재시도 대기가 그 사이 취소됐는지 알아보는 데 쓴다. */
|
|
8048
|
+
this.connectGeneration = 0;
|
|
8049
|
+
/**
|
|
8050
|
+
* connect() 가 첫 연결을 시도하는 중인지. 이때의 핸드셰이크 실패는 connect() 가 직접 다시 시도하거나
|
|
8051
|
+
* 폴백하므로, onclose 가 백그라운드 재연결 체인(handleDisconnect)을 따로 시작하면 안 된다 — 두 경로가
|
|
8052
|
+
* 동시에 연결을 열게 된다.
|
|
8053
|
+
*/
|
|
8054
|
+
this.initialConnecting = false;
|
|
8055
|
+
/** 다음 재연결 대기의 하한(ms) — 서버가 알려 준 retry_after_ms. handleDisconnect 가 한 번 쓰고 비운다. */
|
|
8056
|
+
this.reconnectFloorMs = 0;
|
|
8057
|
+
/** connect() 의 재시도 대기를 끝내는 함수 — disconnect() 가 기다림 없이 바로 취소하게 한다. */
|
|
8058
|
+
this.cancelConnectWait = null;
|
|
8001
8059
|
this.pendingRequests = /* @__PURE__ */ new Map();
|
|
8002
8060
|
this.subscriptions = /* @__PURE__ */ new Map();
|
|
8003
8061
|
this.streamSessions = /* @__PURE__ */ new Map();
|
|
@@ -8054,12 +8112,12 @@ var RealtimeAPI = class {
|
|
|
8054
8112
|
);
|
|
8055
8113
|
}
|
|
8056
8114
|
this.connectPromise = this.primeDirectRealtime().then(
|
|
8057
|
-
() => this.
|
|
8115
|
+
() => this.connectWithRetry()
|
|
8058
8116
|
);
|
|
8059
8117
|
try {
|
|
8060
8118
|
await this.connectPromise;
|
|
8061
8119
|
} catch (e) {
|
|
8062
|
-
if (e instanceof RealtimeConnectionError) {
|
|
8120
|
+
if (e instanceof RealtimeConnectionError || e instanceof ConnectCancelledError) {
|
|
8063
8121
|
throw e;
|
|
8064
8122
|
}
|
|
8065
8123
|
const hasAuth = !!(this.options.accessToken || this.http.getPublicKey());
|
|
@@ -8105,6 +8163,8 @@ var RealtimeAPI = class {
|
|
|
8105
8163
|
* 연결 해제
|
|
8106
8164
|
*/
|
|
8107
8165
|
disconnect() {
|
|
8166
|
+
this.connectGeneration++;
|
|
8167
|
+
this.cancelConnectWait?.();
|
|
8108
8168
|
this.state = "disconnected";
|
|
8109
8169
|
this.notifyStateChange();
|
|
8110
8170
|
if (this.ws) {
|
|
@@ -8163,12 +8223,7 @@ var RealtimeAPI = class {
|
|
|
8163
8223
|
action: "subscribe",
|
|
8164
8224
|
request_id: requestId
|
|
8165
8225
|
});
|
|
8166
|
-
const info =
|
|
8167
|
-
category: response.category,
|
|
8168
|
-
persist: response.persist,
|
|
8169
|
-
historyCount: response.history_count,
|
|
8170
|
-
readReceipt: response.read_receipt
|
|
8171
|
-
};
|
|
8226
|
+
const info = toCategoryInfo(response);
|
|
8172
8227
|
const handlers = [];
|
|
8173
8228
|
this.subscriptions.set(category, { info, handlers });
|
|
8174
8229
|
const subscription = {
|
|
@@ -8224,7 +8279,7 @@ var RealtimeAPI = class {
|
|
|
8224
8279
|
await this.sendRequest({
|
|
8225
8280
|
category,
|
|
8226
8281
|
action: "send",
|
|
8227
|
-
data: { data, broadcast },
|
|
8282
|
+
data: options.to ? { data, broadcast, to: options.to } : { data, broadcast },
|
|
8228
8283
|
request_id: requestId
|
|
8229
8284
|
});
|
|
8230
8285
|
}
|
|
@@ -8943,18 +8998,79 @@ var RealtimeAPI = class {
|
|
|
8943
8998
|
async primeDirectRealtime() {
|
|
8944
8999
|
if (this.directRealtimePrimed) return;
|
|
8945
9000
|
this.directRealtimePrimed = true;
|
|
8946
|
-
if (this.http.getDirectRealtimeBaseUrl()
|
|
9001
|
+
if (this.http.getDirectRealtimeBaseUrl()) {
|
|
8947
9002
|
return;
|
|
8948
9003
|
}
|
|
8949
9004
|
try {
|
|
8950
|
-
await this.http.
|
|
9005
|
+
await this.http.primeEdgeHints(1500);
|
|
8951
9006
|
} catch (e) {
|
|
8952
9007
|
this.log(
|
|
8953
9008
|
`edge hint lookup failed, using default socket URL: ${e instanceof Error ? e.message : String(e)}`
|
|
8954
9009
|
);
|
|
8955
9010
|
}
|
|
8956
9011
|
}
|
|
8957
|
-
|
|
9012
|
+
/**
|
|
9013
|
+
* 첫 연결을 연결 제한 시간(`timeout`) 안에서 다시 시도한다 (6.6.0+, platform-issue 01a15e24).
|
|
9014
|
+
*
|
|
9015
|
+
* - 서버가 재시도 시각을 알려 준 거부(`retryAfterMs`, 예: 한 IP 에서 연결이 몰린 `rate_limited`):
|
|
9016
|
+
* 그 시각에 지터를 더해 다시 붙는다. 공연 시작처럼 한꺼번에 들어올 때 앱이 재시도를 짜지 않아도 된다.
|
|
9017
|
+
* - 이유 없는 핸드셰이크 실패: 두 번까지 백오프 후 다시 한다. 네트워크가 WebSocket 을 실제로 막으면
|
|
9018
|
+
* 매번 실패하고, 앞단의 일시 거부는 다시 하면 붙는다 — 한 번 실패로 SSE 폴백하던 것이 연결 속도
|
|
9019
|
+
* 제한을 "네트워크 차단" 으로 오진한 원인이었다.
|
|
9020
|
+
* - 그 밖의 거부(자격증명, 동시접속 한도 등)와 시간 초과는 바로 던진다.
|
|
9021
|
+
*/
|
|
9022
|
+
async connectWithRetry() {
|
|
9023
|
+
const generation = this.connectGeneration;
|
|
9024
|
+
const deadline = Date.now() + this.options.timeout;
|
|
9025
|
+
let handshakeFailures = 0;
|
|
9026
|
+
this.initialConnecting = true;
|
|
9027
|
+
try {
|
|
9028
|
+
for (let attempt = 1; ; attempt++) {
|
|
9029
|
+
try {
|
|
9030
|
+
await this.doConnect(Math.max(deadline - Date.now(), 1));
|
|
9031
|
+
return;
|
|
9032
|
+
} catch (e) {
|
|
9033
|
+
let wait = null;
|
|
9034
|
+
if (e instanceof RealtimeConnectionError) {
|
|
9035
|
+
if (e.retryable && e.retryAfterMs !== void 0) {
|
|
9036
|
+
wait = e.retryAfterMs + jitter(Math.min(100 * 2 ** attempt, 3e3));
|
|
9037
|
+
} else if (e.code === "rate_limited") {
|
|
9038
|
+
const base = Math.min(500 * 2 ** (attempt - 1), 8e3);
|
|
9039
|
+
wait = base / 2 + jitter(base / 2);
|
|
9040
|
+
}
|
|
9041
|
+
} else if (e instanceof HandshakeFailedError && handshakeFailures < MAX_HANDSHAKE_RETRIES) {
|
|
9042
|
+
handshakeFailures++;
|
|
9043
|
+
const base = 500 * 2 ** (handshakeFailures - 1);
|
|
9044
|
+
wait = base / 2 + jitter(base / 2);
|
|
9045
|
+
}
|
|
9046
|
+
if (wait === null || Date.now() + wait >= deadline) {
|
|
9047
|
+
if (this.state === "connecting") {
|
|
9048
|
+
this.state = "disconnected";
|
|
9049
|
+
this.notifyStateChange();
|
|
9050
|
+
}
|
|
9051
|
+
throw e;
|
|
9052
|
+
}
|
|
9053
|
+
this.log(
|
|
9054
|
+
`Connect attempt ${attempt} failed (${e instanceof Error ? e.message : String(e)}); retrying in ${wait}ms`
|
|
9055
|
+
);
|
|
9056
|
+
await new Promise((resolve) => {
|
|
9057
|
+
const timer = setTimeout(resolve, wait);
|
|
9058
|
+
this.cancelConnectWait = () => {
|
|
9059
|
+
clearTimeout(timer);
|
|
9060
|
+
resolve();
|
|
9061
|
+
};
|
|
9062
|
+
});
|
|
9063
|
+
this.cancelConnectWait = null;
|
|
9064
|
+
if (generation !== this.connectGeneration) {
|
|
9065
|
+
throw new ConnectCancelledError();
|
|
9066
|
+
}
|
|
9067
|
+
}
|
|
9068
|
+
}
|
|
9069
|
+
} finally {
|
|
9070
|
+
this.initialConnecting = false;
|
|
9071
|
+
}
|
|
9072
|
+
}
|
|
9073
|
+
async doConnect(timeoutMs = this.options.timeout) {
|
|
8958
9074
|
return new Promise((resolve, reject) => {
|
|
8959
9075
|
this.state = "connecting";
|
|
8960
9076
|
this.notifyStateChange();
|
|
@@ -9000,7 +9116,7 @@ var RealtimeAPI = class {
|
|
|
9000
9116
|
new Error(`Connection timeout after ${this.options.timeout}ms`)
|
|
9001
9117
|
);
|
|
9002
9118
|
}
|
|
9003
|
-
},
|
|
9119
|
+
}, timeoutMs);
|
|
9004
9120
|
try {
|
|
9005
9121
|
this.log(`Connecting to ${wsUrl}`);
|
|
9006
9122
|
this.ws = new WebSocket(url);
|
|
@@ -9063,11 +9179,16 @@ var RealtimeAPI = class {
|
|
|
9063
9179
|
clearTimeout(timeoutId);
|
|
9064
9180
|
if (usingDirect) this.http.disableDirectRealtime();
|
|
9065
9181
|
reject(
|
|
9066
|
-
new
|
|
9182
|
+
new HandshakeFailedError(
|
|
9067
9183
|
`Connection closed: ${event.reason || "unknown reason"}`
|
|
9068
9184
|
)
|
|
9069
9185
|
);
|
|
9070
9186
|
}
|
|
9187
|
+
if (this.initialConnecting && this.state === "connecting") {
|
|
9188
|
+
this.state = "disconnected";
|
|
9189
|
+
this.ws = null;
|
|
9190
|
+
return;
|
|
9191
|
+
}
|
|
9071
9192
|
if (this.state === "connected" || this.state === "connecting") {
|
|
9072
9193
|
this.handleDisconnect();
|
|
9073
9194
|
}
|
|
@@ -9075,12 +9196,14 @@ var RealtimeAPI = class {
|
|
|
9075
9196
|
this.ws.onerror = (error) => {
|
|
9076
9197
|
this.log("WebSocket error occurred");
|
|
9077
9198
|
this.logError("WebSocket error", error);
|
|
9078
|
-
this.
|
|
9199
|
+
if (!this.initialConnecting) {
|
|
9200
|
+
this.notifyError(new Error("WebSocket connection error"));
|
|
9201
|
+
}
|
|
9079
9202
|
if (!settled && this.state === "connecting") {
|
|
9080
9203
|
settled = true;
|
|
9081
9204
|
clearTimeout(timeoutId);
|
|
9082
9205
|
if (usingDirect) this.http.disableDirectRealtime();
|
|
9083
|
-
reject(new
|
|
9206
|
+
reject(new HandshakeFailedError("Failed to connect"));
|
|
9084
9207
|
}
|
|
9085
9208
|
};
|
|
9086
9209
|
} catch (e) {
|
|
@@ -9144,7 +9267,8 @@ var RealtimeAPI = class {
|
|
|
9144
9267
|
category: data.category,
|
|
9145
9268
|
from: data.from,
|
|
9146
9269
|
data: data.data,
|
|
9147
|
-
sentAt: data.sent_at
|
|
9270
|
+
sentAt: data.sent_at,
|
|
9271
|
+
fromPublisher: data.from_publisher === true
|
|
9148
9272
|
};
|
|
9149
9273
|
sub.handlers.forEach((h) => h(message));
|
|
9150
9274
|
}
|
|
@@ -9155,7 +9279,14 @@ var RealtimeAPI = class {
|
|
|
9155
9279
|
const pending = this.pendingRequests.get(msg.request_id);
|
|
9156
9280
|
if (pending) {
|
|
9157
9281
|
clearTimeout(pending.timeout);
|
|
9158
|
-
|
|
9282
|
+
const errData = msg.data;
|
|
9283
|
+
pending.reject(
|
|
9284
|
+
new RealtimeRequestError({
|
|
9285
|
+
code: errData?.code,
|
|
9286
|
+
message: msg.error || "Unknown error",
|
|
9287
|
+
category: msg.category
|
|
9288
|
+
})
|
|
9289
|
+
);
|
|
9159
9290
|
this.pendingRequests.delete(msg.request_id);
|
|
9160
9291
|
}
|
|
9161
9292
|
} else {
|
|
@@ -9306,8 +9437,10 @@ var RealtimeAPI = class {
|
|
|
9306
9437
|
this.streamSessions.clear();
|
|
9307
9438
|
if (!this.pendingRestore) {
|
|
9308
9439
|
const subscriptions = /* @__PURE__ */ new Map();
|
|
9440
|
+
const infos = /* @__PURE__ */ new Map();
|
|
9309
9441
|
for (const [category, sub] of this.subscriptions) {
|
|
9310
9442
|
subscriptions.set(category, [...sub.handlers]);
|
|
9443
|
+
infos.set(category, sub.info);
|
|
9311
9444
|
}
|
|
9312
9445
|
const presence = /* @__PURE__ */ new Map();
|
|
9313
9446
|
for (const [userId, handlers] of this.presenceSubscriptions) {
|
|
@@ -9317,7 +9450,7 @@ var RealtimeAPI = class {
|
|
|
9317
9450
|
for (const [roomId, handlers] of this.typingHandlers) {
|
|
9318
9451
|
typing.set(roomId, [...handlers]);
|
|
9319
9452
|
}
|
|
9320
|
-
this.pendingRestore = { subscriptions, presence, typing };
|
|
9453
|
+
this.pendingRestore = { subscriptions, infos, presence, typing };
|
|
9321
9454
|
}
|
|
9322
9455
|
this.subscriptions.clear();
|
|
9323
9456
|
this.presenceSubscriptions.clear();
|
|
@@ -9326,10 +9459,15 @@ var RealtimeAPI = class {
|
|
|
9326
9459
|
this.state = "reconnecting";
|
|
9327
9460
|
this.notifyStateChange();
|
|
9328
9461
|
this.retryCount++;
|
|
9329
|
-
const
|
|
9462
|
+
const base = Math.min(
|
|
9330
9463
|
this.options.retryInterval * 2 ** (this.retryCount - 1),
|
|
9331
9464
|
3e4
|
|
9332
9465
|
);
|
|
9466
|
+
const delay = Math.max(
|
|
9467
|
+
Math.ceil(base / 2) + jitter(Math.floor(base / 2)),
|
|
9468
|
+
this.reconnectFloorMs
|
|
9469
|
+
);
|
|
9470
|
+
this.reconnectFloorMs = 0;
|
|
9333
9471
|
setTimeout(async () => {
|
|
9334
9472
|
if (this.sseFallbackActive) return;
|
|
9335
9473
|
if (this.state !== "reconnecting") return;
|
|
@@ -9342,13 +9480,15 @@ var RealtimeAPI = class {
|
|
|
9342
9480
|
await this.restoreSubscriptions(
|
|
9343
9481
|
snapshot.subscriptions,
|
|
9344
9482
|
snapshot.presence,
|
|
9345
|
-
snapshot.typing
|
|
9483
|
+
snapshot.typing,
|
|
9484
|
+
snapshot.infos
|
|
9346
9485
|
);
|
|
9347
9486
|
}
|
|
9348
9487
|
} catch (e) {
|
|
9349
9488
|
this.logError("Reconnect failed", e);
|
|
9350
9489
|
if (e instanceof RealtimeConnectionError) {
|
|
9351
9490
|
if (e.retryable && this.retryCount < this.options.maxRetries) {
|
|
9491
|
+
this.reconnectFloorMs = e.retryAfterMs ?? 0;
|
|
9352
9492
|
this.handleDisconnect();
|
|
9353
9493
|
} else {
|
|
9354
9494
|
this.failRealtime(e);
|
|
@@ -9388,7 +9528,7 @@ var RealtimeAPI = class {
|
|
|
9388
9528
|
* 이전 구독 상태를 복구합니다.
|
|
9389
9529
|
* 재연결 성공 후 호출됩니다.
|
|
9390
9530
|
*/
|
|
9391
|
-
async restoreSubscriptions(previousSubscriptions, previousPresenceSubscriptions, previousTypingHandlers) {
|
|
9531
|
+
async restoreSubscriptions(previousSubscriptions, previousPresenceSubscriptions, previousTypingHandlers, previousInfos) {
|
|
9392
9532
|
for (const [category, handlers] of previousSubscriptions) {
|
|
9393
9533
|
try {
|
|
9394
9534
|
this.log(`Restoring subscription: ${category}`);
|
|
@@ -9398,12 +9538,9 @@ var RealtimeAPI = class {
|
|
|
9398
9538
|
action: "subscribe",
|
|
9399
9539
|
request_id: requestId
|
|
9400
9540
|
});
|
|
9401
|
-
const
|
|
9402
|
-
|
|
9403
|
-
|
|
9404
|
-
historyCount: response.history_count,
|
|
9405
|
-
readReceipt: response.read_receipt
|
|
9406
|
-
};
|
|
9541
|
+
const fresh = toCategoryInfo(response);
|
|
9542
|
+
const info = previousInfos?.get(category) ?? fresh;
|
|
9543
|
+
Object.assign(info, fresh);
|
|
9407
9544
|
this.subscriptions.set(category, { info, handlers });
|
|
9408
9545
|
this.log(`Restored subscription: ${category}`);
|
|
9409
9546
|
} catch (e) {
|
|
@@ -9456,7 +9593,7 @@ var RealtimeAPI = class {
|
|
|
9456
9593
|
assertBidirectional(feature) {
|
|
9457
9594
|
if (this.activeTransport === "sse") {
|
|
9458
9595
|
throw new Error(
|
|
9459
|
-
`realtime.${feature}() requires a WebSocket connection, which
|
|
9596
|
+
`realtime.${feature}() requires a WebSocket connection, which could not be established (the network may block WebSocket, or the connection was repeatedly refused). Only AI streaming (realtime.stream) works in SSE fallback mode (cb.realtime.transport === 'sse').`
|
|
9460
9597
|
);
|
|
9461
9598
|
}
|
|
9462
9599
|
}
|
|
@@ -9610,6 +9747,28 @@ function createTimeoutController(options = {}) {
|
|
|
9610
9747
|
}
|
|
9611
9748
|
};
|
|
9612
9749
|
}
|
|
9750
|
+
function withAbort(promise, signal) {
|
|
9751
|
+
if (!signal) return promise;
|
|
9752
|
+
const reason = () => signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
|
|
9753
|
+
if (signal.aborted) {
|
|
9754
|
+
promise.catch(() => void 0);
|
|
9755
|
+
return Promise.reject(reason());
|
|
9756
|
+
}
|
|
9757
|
+
return new Promise((resolve, reject) => {
|
|
9758
|
+
const onAbort = () => reject(reason());
|
|
9759
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
9760
|
+
promise.then(
|
|
9761
|
+
(value) => {
|
|
9762
|
+
signal.removeEventListener("abort", onAbort);
|
|
9763
|
+
resolve(value);
|
|
9764
|
+
},
|
|
9765
|
+
(error) => {
|
|
9766
|
+
signal.removeEventListener("abort", onAbort);
|
|
9767
|
+
reject(error);
|
|
9768
|
+
}
|
|
9769
|
+
);
|
|
9770
|
+
});
|
|
9771
|
+
}
|
|
9613
9772
|
|
|
9614
9773
|
// src/core/url-validation.ts
|
|
9615
9774
|
var LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1"]);
|
|
@@ -12977,14 +13136,14 @@ var HttpClient = class {
|
|
|
12977
13136
|
if (credential) {
|
|
12978
13137
|
headers["X-Public-Key"] = credential;
|
|
12979
13138
|
}
|
|
12980
|
-
const response = await
|
|
12981
|
-
`${this.config.baseUrl}/v1/auth/re-issue`,
|
|
12982
|
-
{
|
|
13139
|
+
const response = await withAbort(
|
|
13140
|
+
fetch(`${this.config.baseUrl}/v1/auth/re-issue`, {
|
|
12983
13141
|
method: "POST",
|
|
12984
13142
|
headers,
|
|
12985
13143
|
credentials: "include",
|
|
12986
13144
|
signal
|
|
12987
|
-
}
|
|
13145
|
+
}),
|
|
13146
|
+
signal
|
|
12988
13147
|
);
|
|
12989
13148
|
if (!response.ok) {
|
|
12990
13149
|
const status = response.status;
|
|
@@ -13321,40 +13480,73 @@ var HttpClient = class {
|
|
|
13321
13480
|
*/
|
|
13322
13481
|
learnDirectOrigin(response) {
|
|
13323
13482
|
if (!response.headers) return;
|
|
13483
|
+
this.adoptDirectBases(
|
|
13484
|
+
response.headers.get("X-CB-Direct-Base"),
|
|
13485
|
+
response.headers.get("X-CB-Realtime-Direct-Base")
|
|
13486
|
+
);
|
|
13487
|
+
}
|
|
13488
|
+
/**
|
|
13489
|
+
* 광고된 direct 원본을 검증해 받아들인다. 응답 헤더(learnDirectOrigin)와 edge-hints 본문
|
|
13490
|
+
* (primeEdgeHints)이 같은 검증을 거친다.
|
|
13491
|
+
*/
|
|
13492
|
+
adoptDirectBases(advertised, advertisedRealtime) {
|
|
13324
13493
|
const tail = (h) => h.split(".").slice(-2).join(".");
|
|
13325
13494
|
let base = null;
|
|
13326
13495
|
try {
|
|
13327
13496
|
base = new URL(this.config.baseUrl);
|
|
13328
13497
|
} catch {
|
|
13329
13498
|
}
|
|
13330
|
-
if (base && !this.directDisabled && !this.directBaseUrl) {
|
|
13331
|
-
|
|
13332
|
-
|
|
13333
|
-
|
|
13334
|
-
|
|
13335
|
-
if (next.protocol === "https:" && tail(next.hostname) === tail(base.hostname)) {
|
|
13336
|
-
this.directBaseUrl = next.origin;
|
|
13337
|
-
}
|
|
13338
|
-
} catch {
|
|
13499
|
+
if (base && !this.directDisabled && !this.directBaseUrl && typeof advertised === "string" && advertised) {
|
|
13500
|
+
try {
|
|
13501
|
+
const next = new URL(advertised);
|
|
13502
|
+
if (next.protocol === "https:" && tail(next.hostname) === tail(base.hostname)) {
|
|
13503
|
+
this.directBaseUrl = next.origin;
|
|
13339
13504
|
}
|
|
13505
|
+
} catch {
|
|
13340
13506
|
}
|
|
13341
13507
|
}
|
|
13342
|
-
if (!this.directRealtimeDisabled && !this.directRealtimeBaseUrl) {
|
|
13508
|
+
if (!this.directRealtimeDisabled && !this.directRealtimeBaseUrl && typeof advertisedRealtime === "string" && advertisedRealtime) {
|
|
13343
13509
|
const expectedTail = base ? tail(base.hostname) : "connectbase.world";
|
|
13344
|
-
|
|
13345
|
-
|
|
13346
|
-
|
|
13347
|
-
|
|
13348
|
-
try {
|
|
13349
|
-
const next = new URL(advertisedRealtime);
|
|
13350
|
-
if (next.protocol === "wss:" && tail(next.hostname) === expectedTail) {
|
|
13351
|
-
this.directRealtimeBaseUrl = next.origin;
|
|
13352
|
-
}
|
|
13353
|
-
} catch {
|
|
13510
|
+
try {
|
|
13511
|
+
const next = new URL(advertisedRealtime);
|
|
13512
|
+
if (next.protocol === "wss:" && tail(next.hostname) === expectedTail) {
|
|
13513
|
+
this.directRealtimeBaseUrl = next.origin;
|
|
13354
13514
|
}
|
|
13515
|
+
} catch {
|
|
13355
13516
|
}
|
|
13356
13517
|
}
|
|
13357
13518
|
}
|
|
13519
|
+
/**
|
|
13520
|
+
* realtime 첫 연결 전에 저지연 direct 원본 힌트를 묻는다 (`GET /v1/public/edge-hints`, 6.6.0+).
|
|
13521
|
+
*
|
|
13522
|
+
* **커스텀 헤더 없는 단순 GET** 이다. `X-Public-Key` 같은 헤더가 붙으면 브라우저가 CORS 사전 요청을
|
|
13523
|
+
* 먼저 보내 Cloudflare 를 두 번 왕복하고, 앱의 "로컬 개발 origin 허용" 이 꺼진 localhost 페이지에서는
|
|
13524
|
+
* 키 기반 opt-in 에 막혀 브라우저만 direct 를 못 썼다(Node 는 썼다 — platform-issue 01a15e24).
|
|
13525
|
+
* 서버는 이 경로를 키 없이 열어 두고 같은 값을 본문으로도 준다. 본문을 읽으므로 CORS
|
|
13526
|
+
* Expose-Headers 에도 기대지 않는다. 실패는 조용히 무시한다 — direct 는 최적화일 뿐이다.
|
|
13527
|
+
*/
|
|
13528
|
+
async primeEdgeHints(timeoutMs) {
|
|
13529
|
+
if (this.directRealtimeBaseUrl || this.directRealtimeDisabled) return;
|
|
13530
|
+
const { signal, cleanup } = createTimeoutController({ timeout: timeoutMs });
|
|
13531
|
+
try {
|
|
13532
|
+
const response = await withAbort(
|
|
13533
|
+
fetch(`${this.config.baseUrl}/v1/public/edge-hints`, {
|
|
13534
|
+
method: "GET",
|
|
13535
|
+
credentials: "omit",
|
|
13536
|
+
signal
|
|
13537
|
+
}),
|
|
13538
|
+
signal
|
|
13539
|
+
);
|
|
13540
|
+
if (!response.ok) {
|
|
13541
|
+
throw new Error(`edge-hints HTTP ${response.status}`);
|
|
13542
|
+
}
|
|
13543
|
+
this.learnDirectOrigin(response);
|
|
13544
|
+
const body = await withAbort(response.json(), signal);
|
|
13545
|
+
this.adoptDirectBases(body?.direct_base, body?.realtime_direct_base);
|
|
13546
|
+
} finally {
|
|
13547
|
+
cleanup();
|
|
13548
|
+
}
|
|
13549
|
+
}
|
|
13358
13550
|
/**
|
|
13359
13551
|
* SDK 인증 헤더를 **주입하지 않는** raw fetch. 저지연 원본 선택과 폴백만 얹는다.
|
|
13360
13552
|
*
|
|
@@ -13386,11 +13578,14 @@ var HttpClient = class {
|
|
|
13386
13578
|
});
|
|
13387
13579
|
const origin = this.resolveOrigin(url, config?.baseUrl);
|
|
13388
13580
|
try {
|
|
13389
|
-
const response = await
|
|
13390
|
-
|
|
13391
|
-
|
|
13581
|
+
const response = await withAbort(
|
|
13582
|
+
fetch(`${origin}${url}`, {
|
|
13583
|
+
...init,
|
|
13584
|
+
credentials: fetchCredentialsForPath(url),
|
|
13585
|
+
signal
|
|
13586
|
+
}),
|
|
13392
13587
|
signal
|
|
13393
|
-
|
|
13588
|
+
);
|
|
13394
13589
|
if (!config?.baseUrl) {
|
|
13395
13590
|
this.learnDirectOrigin(response);
|
|
13396
13591
|
}
|
|
@@ -13398,11 +13593,14 @@ var HttpClient = class {
|
|
|
13398
13593
|
} catch (err) {
|
|
13399
13594
|
if (!config?.baseUrl && origin !== this.config.baseUrl && !isAbortError(err)) {
|
|
13400
13595
|
this.directDisabled = true;
|
|
13401
|
-
const response = await
|
|
13402
|
-
|
|
13403
|
-
|
|
13596
|
+
const response = await withAbort(
|
|
13597
|
+
fetch(`${this.config.baseUrl}${url}`, {
|
|
13598
|
+
...init,
|
|
13599
|
+
credentials: fetchCredentialsForPath(url),
|
|
13600
|
+
signal
|
|
13601
|
+
}),
|
|
13404
13602
|
signal
|
|
13405
|
-
|
|
13603
|
+
);
|
|
13406
13604
|
return { response, ok: response.ok, status: response.status };
|
|
13407
13605
|
}
|
|
13408
13606
|
throw err;
|
|
@@ -14362,6 +14560,7 @@ var index_default = ConnectBase;
|
|
|
14362
14560
|
NativeAPI,
|
|
14363
14561
|
OrganizationsAPI,
|
|
14364
14562
|
RealtimeConnectionError,
|
|
14563
|
+
RealtimeRequestError,
|
|
14365
14564
|
RolesAPI,
|
|
14366
14565
|
SessionManager,
|
|
14367
14566
|
VideoProcessingError,
|