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.mjs
CHANGED
|
@@ -240,6 +240,7 @@ var RealtimeConnectionError = class extends Error {
|
|
|
240
240
|
this.closeCode = init.closeCode;
|
|
241
241
|
this.retryable = init.retryable ?? isRetryableRealtimeRejection(this.code);
|
|
242
242
|
this.details = init.details;
|
|
243
|
+
this.retryAfterMs = init.retryAfterMs;
|
|
243
244
|
}
|
|
244
245
|
/** `JSON.stringify(err)` 가 `{}` 가 되지 않도록 한다 (ApiError.toJSON 참조). */
|
|
245
246
|
toJSON() {
|
|
@@ -249,13 +250,31 @@ var RealtimeConnectionError = class extends Error {
|
|
|
249
250
|
message: this.message,
|
|
250
251
|
closeCode: this.closeCode,
|
|
251
252
|
retryable: this.retryable,
|
|
252
|
-
details: this.details
|
|
253
|
+
details: this.details,
|
|
254
|
+
retryAfterMs: this.retryAfterMs
|
|
253
255
|
};
|
|
254
256
|
}
|
|
255
257
|
};
|
|
256
258
|
function isRetryableRealtimeRejection(code) {
|
|
257
|
-
return code === "connection_limit_exceeded" || code === "server_at_capacity" || code === "auth_unavailable" || code === "internal_error";
|
|
259
|
+
return code === "connection_limit_exceeded" || code === "server_at_capacity" || code === "rate_limited" || code === "auth_unavailable" || code === "internal_error";
|
|
258
260
|
}
|
|
261
|
+
var RealtimeRequestError = class extends Error {
|
|
262
|
+
constructor(init) {
|
|
263
|
+
super(init.message || init.code || "Realtime request failed");
|
|
264
|
+
this.name = "RealtimeRequestError";
|
|
265
|
+
this.code = init.code || "unknown";
|
|
266
|
+
this.category = init.category;
|
|
267
|
+
}
|
|
268
|
+
/** `JSON.stringify(err)` 가 `{}` 가 되지 않도록 한다 (ApiError.toJSON 참조). */
|
|
269
|
+
toJSON() {
|
|
270
|
+
return {
|
|
271
|
+
name: this.name,
|
|
272
|
+
code: this.code,
|
|
273
|
+
message: this.message,
|
|
274
|
+
category: this.category
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
};
|
|
259
278
|
|
|
260
279
|
// src/api/ai.ts
|
|
261
280
|
function aiErrorToApiError(err) {
|
|
@@ -7907,9 +7926,35 @@ function toConnectionRejection(msg) {
|
|
|
7907
7926
|
message: data.message || msg.error,
|
|
7908
7927
|
closeCode: data.close_code,
|
|
7909
7928
|
retryable: data.retryable,
|
|
7910
|
-
details: data.details
|
|
7929
|
+
details: data.details,
|
|
7930
|
+
retryAfterMs: typeof data.retry_after_ms === "number" && data.retry_after_ms > 0 ? data.retry_after_ms : void 0
|
|
7911
7931
|
});
|
|
7912
7932
|
}
|
|
7933
|
+
var HandshakeFailedError = class extends Error {
|
|
7934
|
+
};
|
|
7935
|
+
var ConnectCancelledError = class extends Error {
|
|
7936
|
+
constructor() {
|
|
7937
|
+
super("Realtime connect cancelled by disconnect()");
|
|
7938
|
+
this.name = "AbortError";
|
|
7939
|
+
}
|
|
7940
|
+
};
|
|
7941
|
+
var MAX_HANDSHAKE_RETRIES = 2;
|
|
7942
|
+
function jitter(max) {
|
|
7943
|
+
return Math.floor(Math.random() * Math.max(0, max));
|
|
7944
|
+
}
|
|
7945
|
+
function toSendPolicy(value) {
|
|
7946
|
+
return value === "upstream" || value === "publishers_only" ? value : "open";
|
|
7947
|
+
}
|
|
7948
|
+
function toCategoryInfo(response) {
|
|
7949
|
+
return {
|
|
7950
|
+
category: response.category,
|
|
7951
|
+
persist: response.persist,
|
|
7952
|
+
historyCount: response.history_count,
|
|
7953
|
+
readReceipt: response.read_receipt,
|
|
7954
|
+
isPublisher: response.publisher === true,
|
|
7955
|
+
sendPolicy: toSendPolicy(response.send_policy)
|
|
7956
|
+
};
|
|
7957
|
+
}
|
|
7913
7958
|
var RealtimeAPI = class {
|
|
7914
7959
|
constructor(http, socketUrl) {
|
|
7915
7960
|
this.ws = null;
|
|
@@ -7947,6 +7992,18 @@ var RealtimeAPI = class {
|
|
|
7947
7992
|
this.pendingRestore = null;
|
|
7948
7993
|
/** 첫 연결 전에 direct 원본 힌트를 한 번 물었는지 (primeDirectRealtime). */
|
|
7949
7994
|
this.directRealtimePrimed = false;
|
|
7995
|
+
/** disconnect() 마다 늘린다 — connect() 의 재시도 대기가 그 사이 취소됐는지 알아보는 데 쓴다. */
|
|
7996
|
+
this.connectGeneration = 0;
|
|
7997
|
+
/**
|
|
7998
|
+
* connect() 가 첫 연결을 시도하는 중인지. 이때의 핸드셰이크 실패는 connect() 가 직접 다시 시도하거나
|
|
7999
|
+
* 폴백하므로, onclose 가 백그라운드 재연결 체인(handleDisconnect)을 따로 시작하면 안 된다 — 두 경로가
|
|
8000
|
+
* 동시에 연결을 열게 된다.
|
|
8001
|
+
*/
|
|
8002
|
+
this.initialConnecting = false;
|
|
8003
|
+
/** 다음 재연결 대기의 하한(ms) — 서버가 알려 준 retry_after_ms. handleDisconnect 가 한 번 쓰고 비운다. */
|
|
8004
|
+
this.reconnectFloorMs = 0;
|
|
8005
|
+
/** connect() 의 재시도 대기를 끝내는 함수 — disconnect() 가 기다림 없이 바로 취소하게 한다. */
|
|
8006
|
+
this.cancelConnectWait = null;
|
|
7950
8007
|
this.pendingRequests = /* @__PURE__ */ new Map();
|
|
7951
8008
|
this.subscriptions = /* @__PURE__ */ new Map();
|
|
7952
8009
|
this.streamSessions = /* @__PURE__ */ new Map();
|
|
@@ -8003,12 +8060,12 @@ var RealtimeAPI = class {
|
|
|
8003
8060
|
);
|
|
8004
8061
|
}
|
|
8005
8062
|
this.connectPromise = this.primeDirectRealtime().then(
|
|
8006
|
-
() => this.
|
|
8063
|
+
() => this.connectWithRetry()
|
|
8007
8064
|
);
|
|
8008
8065
|
try {
|
|
8009
8066
|
await this.connectPromise;
|
|
8010
8067
|
} catch (e) {
|
|
8011
|
-
if (e instanceof RealtimeConnectionError) {
|
|
8068
|
+
if (e instanceof RealtimeConnectionError || e instanceof ConnectCancelledError) {
|
|
8012
8069
|
throw e;
|
|
8013
8070
|
}
|
|
8014
8071
|
const hasAuth = !!(this.options.accessToken || this.http.getPublicKey());
|
|
@@ -8054,6 +8111,8 @@ var RealtimeAPI = class {
|
|
|
8054
8111
|
* 연결 해제
|
|
8055
8112
|
*/
|
|
8056
8113
|
disconnect() {
|
|
8114
|
+
this.connectGeneration++;
|
|
8115
|
+
this.cancelConnectWait?.();
|
|
8057
8116
|
this.state = "disconnected";
|
|
8058
8117
|
this.notifyStateChange();
|
|
8059
8118
|
if (this.ws) {
|
|
@@ -8112,12 +8171,7 @@ var RealtimeAPI = class {
|
|
|
8112
8171
|
action: "subscribe",
|
|
8113
8172
|
request_id: requestId
|
|
8114
8173
|
});
|
|
8115
|
-
const info =
|
|
8116
|
-
category: response.category,
|
|
8117
|
-
persist: response.persist,
|
|
8118
|
-
historyCount: response.history_count,
|
|
8119
|
-
readReceipt: response.read_receipt
|
|
8120
|
-
};
|
|
8174
|
+
const info = toCategoryInfo(response);
|
|
8121
8175
|
const handlers = [];
|
|
8122
8176
|
this.subscriptions.set(category, { info, handlers });
|
|
8123
8177
|
const subscription = {
|
|
@@ -8173,7 +8227,7 @@ var RealtimeAPI = class {
|
|
|
8173
8227
|
await this.sendRequest({
|
|
8174
8228
|
category,
|
|
8175
8229
|
action: "send",
|
|
8176
|
-
data: { data, broadcast },
|
|
8230
|
+
data: options.to ? { data, broadcast, to: options.to } : { data, broadcast },
|
|
8177
8231
|
request_id: requestId
|
|
8178
8232
|
});
|
|
8179
8233
|
}
|
|
@@ -8892,18 +8946,79 @@ var RealtimeAPI = class {
|
|
|
8892
8946
|
async primeDirectRealtime() {
|
|
8893
8947
|
if (this.directRealtimePrimed) return;
|
|
8894
8948
|
this.directRealtimePrimed = true;
|
|
8895
|
-
if (this.http.getDirectRealtimeBaseUrl()
|
|
8949
|
+
if (this.http.getDirectRealtimeBaseUrl()) {
|
|
8896
8950
|
return;
|
|
8897
8951
|
}
|
|
8898
8952
|
try {
|
|
8899
|
-
await this.http.
|
|
8953
|
+
await this.http.primeEdgeHints(1500);
|
|
8900
8954
|
} catch (e) {
|
|
8901
8955
|
this.log(
|
|
8902
8956
|
`edge hint lookup failed, using default socket URL: ${e instanceof Error ? e.message : String(e)}`
|
|
8903
8957
|
);
|
|
8904
8958
|
}
|
|
8905
8959
|
}
|
|
8906
|
-
|
|
8960
|
+
/**
|
|
8961
|
+
* 첫 연결을 연결 제한 시간(`timeout`) 안에서 다시 시도한다 (6.6.0+, platform-issue 01a15e24).
|
|
8962
|
+
*
|
|
8963
|
+
* - 서버가 재시도 시각을 알려 준 거부(`retryAfterMs`, 예: 한 IP 에서 연결이 몰린 `rate_limited`):
|
|
8964
|
+
* 그 시각에 지터를 더해 다시 붙는다. 공연 시작처럼 한꺼번에 들어올 때 앱이 재시도를 짜지 않아도 된다.
|
|
8965
|
+
* - 이유 없는 핸드셰이크 실패: 두 번까지 백오프 후 다시 한다. 네트워크가 WebSocket 을 실제로 막으면
|
|
8966
|
+
* 매번 실패하고, 앞단의 일시 거부는 다시 하면 붙는다 — 한 번 실패로 SSE 폴백하던 것이 연결 속도
|
|
8967
|
+
* 제한을 "네트워크 차단" 으로 오진한 원인이었다.
|
|
8968
|
+
* - 그 밖의 거부(자격증명, 동시접속 한도 등)와 시간 초과는 바로 던진다.
|
|
8969
|
+
*/
|
|
8970
|
+
async connectWithRetry() {
|
|
8971
|
+
const generation = this.connectGeneration;
|
|
8972
|
+
const deadline = Date.now() + this.options.timeout;
|
|
8973
|
+
let handshakeFailures = 0;
|
|
8974
|
+
this.initialConnecting = true;
|
|
8975
|
+
try {
|
|
8976
|
+
for (let attempt = 1; ; attempt++) {
|
|
8977
|
+
try {
|
|
8978
|
+
await this.doConnect(Math.max(deadline - Date.now(), 1));
|
|
8979
|
+
return;
|
|
8980
|
+
} catch (e) {
|
|
8981
|
+
let wait = null;
|
|
8982
|
+
if (e instanceof RealtimeConnectionError) {
|
|
8983
|
+
if (e.retryable && e.retryAfterMs !== void 0) {
|
|
8984
|
+
wait = e.retryAfterMs + jitter(Math.min(100 * 2 ** attempt, 3e3));
|
|
8985
|
+
} else if (e.code === "rate_limited") {
|
|
8986
|
+
const base = Math.min(500 * 2 ** (attempt - 1), 8e3);
|
|
8987
|
+
wait = base / 2 + jitter(base / 2);
|
|
8988
|
+
}
|
|
8989
|
+
} else if (e instanceof HandshakeFailedError && handshakeFailures < MAX_HANDSHAKE_RETRIES) {
|
|
8990
|
+
handshakeFailures++;
|
|
8991
|
+
const base = 500 * 2 ** (handshakeFailures - 1);
|
|
8992
|
+
wait = base / 2 + jitter(base / 2);
|
|
8993
|
+
}
|
|
8994
|
+
if (wait === null || Date.now() + wait >= deadline) {
|
|
8995
|
+
if (this.state === "connecting") {
|
|
8996
|
+
this.state = "disconnected";
|
|
8997
|
+
this.notifyStateChange();
|
|
8998
|
+
}
|
|
8999
|
+
throw e;
|
|
9000
|
+
}
|
|
9001
|
+
this.log(
|
|
9002
|
+
`Connect attempt ${attempt} failed (${e instanceof Error ? e.message : String(e)}); retrying in ${wait}ms`
|
|
9003
|
+
);
|
|
9004
|
+
await new Promise((resolve) => {
|
|
9005
|
+
const timer = setTimeout(resolve, wait);
|
|
9006
|
+
this.cancelConnectWait = () => {
|
|
9007
|
+
clearTimeout(timer);
|
|
9008
|
+
resolve();
|
|
9009
|
+
};
|
|
9010
|
+
});
|
|
9011
|
+
this.cancelConnectWait = null;
|
|
9012
|
+
if (generation !== this.connectGeneration) {
|
|
9013
|
+
throw new ConnectCancelledError();
|
|
9014
|
+
}
|
|
9015
|
+
}
|
|
9016
|
+
}
|
|
9017
|
+
} finally {
|
|
9018
|
+
this.initialConnecting = false;
|
|
9019
|
+
}
|
|
9020
|
+
}
|
|
9021
|
+
async doConnect(timeoutMs = this.options.timeout) {
|
|
8907
9022
|
return new Promise((resolve, reject) => {
|
|
8908
9023
|
this.state = "connecting";
|
|
8909
9024
|
this.notifyStateChange();
|
|
@@ -8949,7 +9064,7 @@ var RealtimeAPI = class {
|
|
|
8949
9064
|
new Error(`Connection timeout after ${this.options.timeout}ms`)
|
|
8950
9065
|
);
|
|
8951
9066
|
}
|
|
8952
|
-
},
|
|
9067
|
+
}, timeoutMs);
|
|
8953
9068
|
try {
|
|
8954
9069
|
this.log(`Connecting to ${wsUrl}`);
|
|
8955
9070
|
this.ws = new WebSocket(url);
|
|
@@ -9012,11 +9127,16 @@ var RealtimeAPI = class {
|
|
|
9012
9127
|
clearTimeout(timeoutId);
|
|
9013
9128
|
if (usingDirect) this.http.disableDirectRealtime();
|
|
9014
9129
|
reject(
|
|
9015
|
-
new
|
|
9130
|
+
new HandshakeFailedError(
|
|
9016
9131
|
`Connection closed: ${event.reason || "unknown reason"}`
|
|
9017
9132
|
)
|
|
9018
9133
|
);
|
|
9019
9134
|
}
|
|
9135
|
+
if (this.initialConnecting && this.state === "connecting") {
|
|
9136
|
+
this.state = "disconnected";
|
|
9137
|
+
this.ws = null;
|
|
9138
|
+
return;
|
|
9139
|
+
}
|
|
9020
9140
|
if (this.state === "connected" || this.state === "connecting") {
|
|
9021
9141
|
this.handleDisconnect();
|
|
9022
9142
|
}
|
|
@@ -9024,12 +9144,14 @@ var RealtimeAPI = class {
|
|
|
9024
9144
|
this.ws.onerror = (error) => {
|
|
9025
9145
|
this.log("WebSocket error occurred");
|
|
9026
9146
|
this.logError("WebSocket error", error);
|
|
9027
|
-
this.
|
|
9147
|
+
if (!this.initialConnecting) {
|
|
9148
|
+
this.notifyError(new Error("WebSocket connection error"));
|
|
9149
|
+
}
|
|
9028
9150
|
if (!settled && this.state === "connecting") {
|
|
9029
9151
|
settled = true;
|
|
9030
9152
|
clearTimeout(timeoutId);
|
|
9031
9153
|
if (usingDirect) this.http.disableDirectRealtime();
|
|
9032
|
-
reject(new
|
|
9154
|
+
reject(new HandshakeFailedError("Failed to connect"));
|
|
9033
9155
|
}
|
|
9034
9156
|
};
|
|
9035
9157
|
} catch (e) {
|
|
@@ -9093,7 +9215,8 @@ var RealtimeAPI = class {
|
|
|
9093
9215
|
category: data.category,
|
|
9094
9216
|
from: data.from,
|
|
9095
9217
|
data: data.data,
|
|
9096
|
-
sentAt: data.sent_at
|
|
9218
|
+
sentAt: data.sent_at,
|
|
9219
|
+
fromPublisher: data.from_publisher === true
|
|
9097
9220
|
};
|
|
9098
9221
|
sub.handlers.forEach((h) => h(message));
|
|
9099
9222
|
}
|
|
@@ -9104,7 +9227,14 @@ var RealtimeAPI = class {
|
|
|
9104
9227
|
const pending = this.pendingRequests.get(msg.request_id);
|
|
9105
9228
|
if (pending) {
|
|
9106
9229
|
clearTimeout(pending.timeout);
|
|
9107
|
-
|
|
9230
|
+
const errData = msg.data;
|
|
9231
|
+
pending.reject(
|
|
9232
|
+
new RealtimeRequestError({
|
|
9233
|
+
code: errData?.code,
|
|
9234
|
+
message: msg.error || "Unknown error",
|
|
9235
|
+
category: msg.category
|
|
9236
|
+
})
|
|
9237
|
+
);
|
|
9108
9238
|
this.pendingRequests.delete(msg.request_id);
|
|
9109
9239
|
}
|
|
9110
9240
|
} else {
|
|
@@ -9255,8 +9385,10 @@ var RealtimeAPI = class {
|
|
|
9255
9385
|
this.streamSessions.clear();
|
|
9256
9386
|
if (!this.pendingRestore) {
|
|
9257
9387
|
const subscriptions = /* @__PURE__ */ new Map();
|
|
9388
|
+
const infos = /* @__PURE__ */ new Map();
|
|
9258
9389
|
for (const [category, sub] of this.subscriptions) {
|
|
9259
9390
|
subscriptions.set(category, [...sub.handlers]);
|
|
9391
|
+
infos.set(category, sub.info);
|
|
9260
9392
|
}
|
|
9261
9393
|
const presence = /* @__PURE__ */ new Map();
|
|
9262
9394
|
for (const [userId, handlers] of this.presenceSubscriptions) {
|
|
@@ -9266,7 +9398,7 @@ var RealtimeAPI = class {
|
|
|
9266
9398
|
for (const [roomId, handlers] of this.typingHandlers) {
|
|
9267
9399
|
typing.set(roomId, [...handlers]);
|
|
9268
9400
|
}
|
|
9269
|
-
this.pendingRestore = { subscriptions, presence, typing };
|
|
9401
|
+
this.pendingRestore = { subscriptions, infos, presence, typing };
|
|
9270
9402
|
}
|
|
9271
9403
|
this.subscriptions.clear();
|
|
9272
9404
|
this.presenceSubscriptions.clear();
|
|
@@ -9275,10 +9407,15 @@ var RealtimeAPI = class {
|
|
|
9275
9407
|
this.state = "reconnecting";
|
|
9276
9408
|
this.notifyStateChange();
|
|
9277
9409
|
this.retryCount++;
|
|
9278
|
-
const
|
|
9410
|
+
const base = Math.min(
|
|
9279
9411
|
this.options.retryInterval * 2 ** (this.retryCount - 1),
|
|
9280
9412
|
3e4
|
|
9281
9413
|
);
|
|
9414
|
+
const delay = Math.max(
|
|
9415
|
+
Math.ceil(base / 2) + jitter(Math.floor(base / 2)),
|
|
9416
|
+
this.reconnectFloorMs
|
|
9417
|
+
);
|
|
9418
|
+
this.reconnectFloorMs = 0;
|
|
9282
9419
|
setTimeout(async () => {
|
|
9283
9420
|
if (this.sseFallbackActive) return;
|
|
9284
9421
|
if (this.state !== "reconnecting") return;
|
|
@@ -9291,13 +9428,15 @@ var RealtimeAPI = class {
|
|
|
9291
9428
|
await this.restoreSubscriptions(
|
|
9292
9429
|
snapshot.subscriptions,
|
|
9293
9430
|
snapshot.presence,
|
|
9294
|
-
snapshot.typing
|
|
9431
|
+
snapshot.typing,
|
|
9432
|
+
snapshot.infos
|
|
9295
9433
|
);
|
|
9296
9434
|
}
|
|
9297
9435
|
} catch (e) {
|
|
9298
9436
|
this.logError("Reconnect failed", e);
|
|
9299
9437
|
if (e instanceof RealtimeConnectionError) {
|
|
9300
9438
|
if (e.retryable && this.retryCount < this.options.maxRetries) {
|
|
9439
|
+
this.reconnectFloorMs = e.retryAfterMs ?? 0;
|
|
9301
9440
|
this.handleDisconnect();
|
|
9302
9441
|
} else {
|
|
9303
9442
|
this.failRealtime(e);
|
|
@@ -9337,7 +9476,7 @@ var RealtimeAPI = class {
|
|
|
9337
9476
|
* 이전 구독 상태를 복구합니다.
|
|
9338
9477
|
* 재연결 성공 후 호출됩니다.
|
|
9339
9478
|
*/
|
|
9340
|
-
async restoreSubscriptions(previousSubscriptions, previousPresenceSubscriptions, previousTypingHandlers) {
|
|
9479
|
+
async restoreSubscriptions(previousSubscriptions, previousPresenceSubscriptions, previousTypingHandlers, previousInfos) {
|
|
9341
9480
|
for (const [category, handlers] of previousSubscriptions) {
|
|
9342
9481
|
try {
|
|
9343
9482
|
this.log(`Restoring subscription: ${category}`);
|
|
@@ -9347,12 +9486,9 @@ var RealtimeAPI = class {
|
|
|
9347
9486
|
action: "subscribe",
|
|
9348
9487
|
request_id: requestId
|
|
9349
9488
|
});
|
|
9350
|
-
const
|
|
9351
|
-
|
|
9352
|
-
|
|
9353
|
-
historyCount: response.history_count,
|
|
9354
|
-
readReceipt: response.read_receipt
|
|
9355
|
-
};
|
|
9489
|
+
const fresh = toCategoryInfo(response);
|
|
9490
|
+
const info = previousInfos?.get(category) ?? fresh;
|
|
9491
|
+
Object.assign(info, fresh);
|
|
9356
9492
|
this.subscriptions.set(category, { info, handlers });
|
|
9357
9493
|
this.log(`Restored subscription: ${category}`);
|
|
9358
9494
|
} catch (e) {
|
|
@@ -9405,7 +9541,7 @@ var RealtimeAPI = class {
|
|
|
9405
9541
|
assertBidirectional(feature) {
|
|
9406
9542
|
if (this.activeTransport === "sse") {
|
|
9407
9543
|
throw new Error(
|
|
9408
|
-
`realtime.${feature}() requires a WebSocket connection, which
|
|
9544
|
+
`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').`
|
|
9409
9545
|
);
|
|
9410
9546
|
}
|
|
9411
9547
|
}
|
|
@@ -9559,6 +9695,28 @@ function createTimeoutController(options = {}) {
|
|
|
9559
9695
|
}
|
|
9560
9696
|
};
|
|
9561
9697
|
}
|
|
9698
|
+
function withAbort(promise, signal) {
|
|
9699
|
+
if (!signal) return promise;
|
|
9700
|
+
const reason = () => signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
|
|
9701
|
+
if (signal.aborted) {
|
|
9702
|
+
promise.catch(() => void 0);
|
|
9703
|
+
return Promise.reject(reason());
|
|
9704
|
+
}
|
|
9705
|
+
return new Promise((resolve, reject) => {
|
|
9706
|
+
const onAbort = () => reject(reason());
|
|
9707
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
9708
|
+
promise.then(
|
|
9709
|
+
(value) => {
|
|
9710
|
+
signal.removeEventListener("abort", onAbort);
|
|
9711
|
+
resolve(value);
|
|
9712
|
+
},
|
|
9713
|
+
(error) => {
|
|
9714
|
+
signal.removeEventListener("abort", onAbort);
|
|
9715
|
+
reject(error);
|
|
9716
|
+
}
|
|
9717
|
+
);
|
|
9718
|
+
});
|
|
9719
|
+
}
|
|
9562
9720
|
|
|
9563
9721
|
// src/core/url-validation.ts
|
|
9564
9722
|
var LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1"]);
|
|
@@ -12926,14 +13084,14 @@ var HttpClient = class {
|
|
|
12926
13084
|
if (credential) {
|
|
12927
13085
|
headers["X-Public-Key"] = credential;
|
|
12928
13086
|
}
|
|
12929
|
-
const response = await
|
|
12930
|
-
`${this.config.baseUrl}/v1/auth/re-issue`,
|
|
12931
|
-
{
|
|
13087
|
+
const response = await withAbort(
|
|
13088
|
+
fetch(`${this.config.baseUrl}/v1/auth/re-issue`, {
|
|
12932
13089
|
method: "POST",
|
|
12933
13090
|
headers,
|
|
12934
13091
|
credentials: "include",
|
|
12935
13092
|
signal
|
|
12936
|
-
}
|
|
13093
|
+
}),
|
|
13094
|
+
signal
|
|
12937
13095
|
);
|
|
12938
13096
|
if (!response.ok) {
|
|
12939
13097
|
const status = response.status;
|
|
@@ -13270,40 +13428,73 @@ var HttpClient = class {
|
|
|
13270
13428
|
*/
|
|
13271
13429
|
learnDirectOrigin(response) {
|
|
13272
13430
|
if (!response.headers) return;
|
|
13431
|
+
this.adoptDirectBases(
|
|
13432
|
+
response.headers.get("X-CB-Direct-Base"),
|
|
13433
|
+
response.headers.get("X-CB-Realtime-Direct-Base")
|
|
13434
|
+
);
|
|
13435
|
+
}
|
|
13436
|
+
/**
|
|
13437
|
+
* 광고된 direct 원본을 검증해 받아들인다. 응답 헤더(learnDirectOrigin)와 edge-hints 본문
|
|
13438
|
+
* (primeEdgeHints)이 같은 검증을 거친다.
|
|
13439
|
+
*/
|
|
13440
|
+
adoptDirectBases(advertised, advertisedRealtime) {
|
|
13273
13441
|
const tail = (h) => h.split(".").slice(-2).join(".");
|
|
13274
13442
|
let base = null;
|
|
13275
13443
|
try {
|
|
13276
13444
|
base = new URL(this.config.baseUrl);
|
|
13277
13445
|
} catch {
|
|
13278
13446
|
}
|
|
13279
|
-
if (base && !this.directDisabled && !this.directBaseUrl) {
|
|
13280
|
-
|
|
13281
|
-
|
|
13282
|
-
|
|
13283
|
-
|
|
13284
|
-
if (next.protocol === "https:" && tail(next.hostname) === tail(base.hostname)) {
|
|
13285
|
-
this.directBaseUrl = next.origin;
|
|
13286
|
-
}
|
|
13287
|
-
} catch {
|
|
13447
|
+
if (base && !this.directDisabled && !this.directBaseUrl && typeof advertised === "string" && advertised) {
|
|
13448
|
+
try {
|
|
13449
|
+
const next = new URL(advertised);
|
|
13450
|
+
if (next.protocol === "https:" && tail(next.hostname) === tail(base.hostname)) {
|
|
13451
|
+
this.directBaseUrl = next.origin;
|
|
13288
13452
|
}
|
|
13453
|
+
} catch {
|
|
13289
13454
|
}
|
|
13290
13455
|
}
|
|
13291
|
-
if (!this.directRealtimeDisabled && !this.directRealtimeBaseUrl) {
|
|
13456
|
+
if (!this.directRealtimeDisabled && !this.directRealtimeBaseUrl && typeof advertisedRealtime === "string" && advertisedRealtime) {
|
|
13292
13457
|
const expectedTail = base ? tail(base.hostname) : "connectbase.world";
|
|
13293
|
-
|
|
13294
|
-
|
|
13295
|
-
|
|
13296
|
-
|
|
13297
|
-
try {
|
|
13298
|
-
const next = new URL(advertisedRealtime);
|
|
13299
|
-
if (next.protocol === "wss:" && tail(next.hostname) === expectedTail) {
|
|
13300
|
-
this.directRealtimeBaseUrl = next.origin;
|
|
13301
|
-
}
|
|
13302
|
-
} catch {
|
|
13458
|
+
try {
|
|
13459
|
+
const next = new URL(advertisedRealtime);
|
|
13460
|
+
if (next.protocol === "wss:" && tail(next.hostname) === expectedTail) {
|
|
13461
|
+
this.directRealtimeBaseUrl = next.origin;
|
|
13303
13462
|
}
|
|
13463
|
+
} catch {
|
|
13304
13464
|
}
|
|
13305
13465
|
}
|
|
13306
13466
|
}
|
|
13467
|
+
/**
|
|
13468
|
+
* realtime 첫 연결 전에 저지연 direct 원본 힌트를 묻는다 (`GET /v1/public/edge-hints`, 6.6.0+).
|
|
13469
|
+
*
|
|
13470
|
+
* **커스텀 헤더 없는 단순 GET** 이다. `X-Public-Key` 같은 헤더가 붙으면 브라우저가 CORS 사전 요청을
|
|
13471
|
+
* 먼저 보내 Cloudflare 를 두 번 왕복하고, 앱의 "로컬 개발 origin 허용" 이 꺼진 localhost 페이지에서는
|
|
13472
|
+
* 키 기반 opt-in 에 막혀 브라우저만 direct 를 못 썼다(Node 는 썼다 — platform-issue 01a15e24).
|
|
13473
|
+
* 서버는 이 경로를 키 없이 열어 두고 같은 값을 본문으로도 준다. 본문을 읽으므로 CORS
|
|
13474
|
+
* Expose-Headers 에도 기대지 않는다. 실패는 조용히 무시한다 — direct 는 최적화일 뿐이다.
|
|
13475
|
+
*/
|
|
13476
|
+
async primeEdgeHints(timeoutMs) {
|
|
13477
|
+
if (this.directRealtimeBaseUrl || this.directRealtimeDisabled) return;
|
|
13478
|
+
const { signal, cleanup } = createTimeoutController({ timeout: timeoutMs });
|
|
13479
|
+
try {
|
|
13480
|
+
const response = await withAbort(
|
|
13481
|
+
fetch(`${this.config.baseUrl}/v1/public/edge-hints`, {
|
|
13482
|
+
method: "GET",
|
|
13483
|
+
credentials: "omit",
|
|
13484
|
+
signal
|
|
13485
|
+
}),
|
|
13486
|
+
signal
|
|
13487
|
+
);
|
|
13488
|
+
if (!response.ok) {
|
|
13489
|
+
throw new Error(`edge-hints HTTP ${response.status}`);
|
|
13490
|
+
}
|
|
13491
|
+
this.learnDirectOrigin(response);
|
|
13492
|
+
const body = await withAbort(response.json(), signal);
|
|
13493
|
+
this.adoptDirectBases(body?.direct_base, body?.realtime_direct_base);
|
|
13494
|
+
} finally {
|
|
13495
|
+
cleanup();
|
|
13496
|
+
}
|
|
13497
|
+
}
|
|
13307
13498
|
/**
|
|
13308
13499
|
* SDK 인증 헤더를 **주입하지 않는** raw fetch. 저지연 원본 선택과 폴백만 얹는다.
|
|
13309
13500
|
*
|
|
@@ -13335,11 +13526,14 @@ var HttpClient = class {
|
|
|
13335
13526
|
});
|
|
13336
13527
|
const origin = this.resolveOrigin(url, config?.baseUrl);
|
|
13337
13528
|
try {
|
|
13338
|
-
const response = await
|
|
13339
|
-
|
|
13340
|
-
|
|
13529
|
+
const response = await withAbort(
|
|
13530
|
+
fetch(`${origin}${url}`, {
|
|
13531
|
+
...init,
|
|
13532
|
+
credentials: fetchCredentialsForPath(url),
|
|
13533
|
+
signal
|
|
13534
|
+
}),
|
|
13341
13535
|
signal
|
|
13342
|
-
|
|
13536
|
+
);
|
|
13343
13537
|
if (!config?.baseUrl) {
|
|
13344
13538
|
this.learnDirectOrigin(response);
|
|
13345
13539
|
}
|
|
@@ -13347,11 +13541,14 @@ var HttpClient = class {
|
|
|
13347
13541
|
} catch (err) {
|
|
13348
13542
|
if (!config?.baseUrl && origin !== this.config.baseUrl && !isAbortError(err)) {
|
|
13349
13543
|
this.directDisabled = true;
|
|
13350
|
-
const response = await
|
|
13351
|
-
|
|
13352
|
-
|
|
13544
|
+
const response = await withAbort(
|
|
13545
|
+
fetch(`${this.config.baseUrl}${url}`, {
|
|
13546
|
+
...init,
|
|
13547
|
+
credentials: fetchCredentialsForPath(url),
|
|
13548
|
+
signal
|
|
13549
|
+
}),
|
|
13353
13550
|
signal
|
|
13354
|
-
|
|
13551
|
+
);
|
|
13355
13552
|
return { response, ok: response.ok, status: response.status };
|
|
13356
13553
|
}
|
|
13357
13554
|
throw err;
|
|
@@ -14310,6 +14507,7 @@ export {
|
|
|
14310
14507
|
NativeAPI,
|
|
14311
14508
|
OrganizationsAPI,
|
|
14312
14509
|
RealtimeConnectionError,
|
|
14510
|
+
RealtimeRequestError,
|
|
14313
14511
|
RolesAPI,
|
|
14314
14512
|
SessionManager,
|
|
14315
14513
|
VideoProcessingError,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "connectbase-client",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.6.1",
|
|
4
4
|
"description": "Connect Base JavaScript/TypeScript SDK for browser and Node.js",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -18,18 +18,24 @@
|
|
|
18
18
|
"main": "dist/index.js",
|
|
19
19
|
"module": "dist/index.mjs",
|
|
20
20
|
"types": "dist/index.d.ts",
|
|
21
|
-
"browser": "dist/connect-base.umd.js",
|
|
22
21
|
"unpkg": "dist/connect-base.umd.js",
|
|
22
|
+
"jsdelivr": "dist/connect-base.umd.js",
|
|
23
23
|
"bin": {
|
|
24
24
|
"connectbase": "dist/cli.js",
|
|
25
25
|
"connectbase-client": "dist/cli.js"
|
|
26
26
|
},
|
|
27
27
|
"exports": {
|
|
28
28
|
".": {
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
"import": {
|
|
30
|
+
"types": "./dist/index.d.mts",
|
|
31
|
+
"default": "./dist/index.mjs"
|
|
32
|
+
},
|
|
33
|
+
"require": {
|
|
34
|
+
"types": "./dist/index.d.ts",
|
|
35
|
+
"default": "./dist/index.js"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"./package.json": "./package.json"
|
|
33
39
|
},
|
|
34
40
|
"files": [
|
|
35
41
|
"dist",
|
|
@@ -45,6 +51,7 @@
|
|
|
45
51
|
"test": "vitest run",
|
|
46
52
|
"test:types": "tsc --noEmit -p test/tsconfig.json",
|
|
47
53
|
"release": "pnpm build && npm publish --access public",
|
|
54
|
+
"publish:cdn": "node scripts/publish-cdn.mjs",
|
|
48
55
|
"lint": "biome lint .",
|
|
49
56
|
"format": "biome format --write .",
|
|
50
57
|
"check": "biome check ."
|
|
@@ -72,6 +79,6 @@
|
|
|
72
79
|
"vitest": "^3.2.4"
|
|
73
80
|
},
|
|
74
81
|
"engines": {
|
|
75
|
-
"node": ">=
|
|
82
|
+
"node": ">=20"
|
|
76
83
|
}
|
|
77
84
|
}
|