connectbase-client 0.6.25 → 0.6.27
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/connect-base.umd.js +2 -2
- package/dist/index.d.mts +198 -1
- package/dist/index.d.ts +198 -1
- package/dist/index.js +487 -5
- package/dist/index.mjs +487 -5
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -464,6 +464,17 @@ var AuthAPI = class {
|
|
|
464
464
|
// src/api/database.ts
|
|
465
465
|
var DatabaseAPI = class {
|
|
466
466
|
constructor(http) {
|
|
467
|
+
this.realtimeWs = null;
|
|
468
|
+
this.realtimeState = "disconnected";
|
|
469
|
+
this.realtimeHandlers = /* @__PURE__ */ new Map();
|
|
470
|
+
this.realtimeRetryCount = 0;
|
|
471
|
+
this.realtimeOptions = null;
|
|
472
|
+
this.pendingRequests = /* @__PURE__ */ new Map();
|
|
473
|
+
this.pingInterval = null;
|
|
474
|
+
this.realtimeOnStateChange = null;
|
|
475
|
+
this.realtimeOnError = null;
|
|
476
|
+
// 재연결 시 재구독용 메타데이터
|
|
477
|
+
this.activeSubscriptions = /* @__PURE__ */ new Map();
|
|
467
478
|
this.http = http;
|
|
468
479
|
}
|
|
469
480
|
/**
|
|
@@ -861,6 +872,425 @@ var DatabaseAPI = class {
|
|
|
861
872
|
`/v1/apps/${appId}/lifecycle/retention/${tableName}`
|
|
862
873
|
);
|
|
863
874
|
}
|
|
875
|
+
// ============ Realtime (Database Subscription) ============
|
|
876
|
+
/**
|
|
877
|
+
* 데이터베이스 실시간 연결
|
|
878
|
+
* data-server의 WebSocket에 연결하여 데이터 변경을 실시간으로 수신합니다.
|
|
879
|
+
*
|
|
880
|
+
* @example
|
|
881
|
+
* ```typescript
|
|
882
|
+
* // 연결
|
|
883
|
+
* cb.database.connectRealtime({
|
|
884
|
+
* accessToken: 'your-jwt-token',
|
|
885
|
+
* dataServerUrl: 'https://data.connectbase.world'
|
|
886
|
+
* })
|
|
887
|
+
*
|
|
888
|
+
* // 테이블 구독
|
|
889
|
+
* const sub = cb.database.subscribe('users', {
|
|
890
|
+
* onSnapshot: (docs, info) => {
|
|
891
|
+
* console.log('Initial data:', docs, 'total:', info.totalCount)
|
|
892
|
+
* },
|
|
893
|
+
* onChange: (changes) => {
|
|
894
|
+
* changes.forEach(change => {
|
|
895
|
+
* console.log(change.type, change.doc_id, change.data)
|
|
896
|
+
* })
|
|
897
|
+
* },
|
|
898
|
+
* onError: (error) => console.error(error)
|
|
899
|
+
* })
|
|
900
|
+
*
|
|
901
|
+
* // 구독 해제
|
|
902
|
+
* sub.unsubscribe()
|
|
903
|
+
*
|
|
904
|
+
* // 연결 해제
|
|
905
|
+
* cb.database.disconnectRealtime()
|
|
906
|
+
* ```
|
|
907
|
+
*/
|
|
908
|
+
connectRealtime(options) {
|
|
909
|
+
if (this.realtimeState === "connected") {
|
|
910
|
+
return Promise.resolve();
|
|
911
|
+
}
|
|
912
|
+
if (this.realtimeState === "connecting") {
|
|
913
|
+
return Promise.reject(new Error("Already connecting"));
|
|
914
|
+
}
|
|
915
|
+
this.realtimeOptions = options;
|
|
916
|
+
this.realtimeRetryCount = 0;
|
|
917
|
+
return this.doRealtimeConnect();
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* 데이터베이스 실시간 연결 해제
|
|
921
|
+
*/
|
|
922
|
+
disconnectRealtime() {
|
|
923
|
+
this.realtimeOptions = null;
|
|
924
|
+
this.setRealtimeState("disconnected");
|
|
925
|
+
this.realtimeRetryCount = 0;
|
|
926
|
+
this.stopRealtimePing();
|
|
927
|
+
if (this.realtimeWs) {
|
|
928
|
+
this.realtimeWs.close();
|
|
929
|
+
this.realtimeWs = null;
|
|
930
|
+
}
|
|
931
|
+
this.pendingRequests.forEach((req) => {
|
|
932
|
+
clearTimeout(req.timeout);
|
|
933
|
+
req.reject(new Error("Connection closed"));
|
|
934
|
+
});
|
|
935
|
+
this.pendingRequests.clear();
|
|
936
|
+
this.realtimeHandlers.clear();
|
|
937
|
+
this.activeSubscriptions.clear();
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* 테이블 또는 문서의 실시간 변경 구독
|
|
941
|
+
*
|
|
942
|
+
* @param tableId 구독할 테이블 이름
|
|
943
|
+
* @param handlers 이벤트 핸들러 (onSnapshot, onChange, onError)
|
|
944
|
+
* @param options 구독 옵션 (docId, where, includeSelf 등)
|
|
945
|
+
* @returns 구독 해제 가능한 객체
|
|
946
|
+
*/
|
|
947
|
+
subscribe(tableId, handlers, options) {
|
|
948
|
+
if (this.realtimeState !== "connected") {
|
|
949
|
+
throw new Error("Not connected. Call connectRealtime() first.");
|
|
950
|
+
}
|
|
951
|
+
const clientSubId = `csub_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
952
|
+
this.activeSubscriptions.set(clientSubId, { tableId, options, handlers });
|
|
953
|
+
const serverSubIdPromise = this.sendSubscribeRequest(tableId, handlers, options);
|
|
954
|
+
serverSubIdPromise.catch((err) => {
|
|
955
|
+
this.activeSubscriptions.delete(clientSubId);
|
|
956
|
+
handlers.onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
957
|
+
});
|
|
958
|
+
return {
|
|
959
|
+
subscriptionId: clientSubId,
|
|
960
|
+
unsubscribe: () => {
|
|
961
|
+
this.activeSubscriptions.delete(clientSubId);
|
|
962
|
+
serverSubIdPromise.then((actualSubId) => {
|
|
963
|
+
this.realtimeHandlers.delete(actualSubId);
|
|
964
|
+
if (this.realtimeState === "connected") {
|
|
965
|
+
this.sendRealtimeMessage({
|
|
966
|
+
type: "unsubscribe",
|
|
967
|
+
request_id: this.generateRequestId(),
|
|
968
|
+
subscription_id: actualSubId
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
}).catch(() => {
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* 프레즌스 상태 설정
|
|
978
|
+
*/
|
|
979
|
+
setPresence(status, device, metadata) {
|
|
980
|
+
if (this.realtimeState !== "connected") return;
|
|
981
|
+
this.sendRealtimeMessage({
|
|
982
|
+
type: "presence_set",
|
|
983
|
+
request_id: this.generateRequestId(),
|
|
984
|
+
status,
|
|
985
|
+
device,
|
|
986
|
+
metadata
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* 프레즌스 구독 (다른 사용자의 온라인 상태 감시)
|
|
991
|
+
*/
|
|
992
|
+
subscribePresence(userIds, onPresence) {
|
|
993
|
+
if (this.realtimeState !== "connected") return;
|
|
994
|
+
this.realtimeHandlers.set("__presence__", {
|
|
995
|
+
onSnapshot: (docs) => {
|
|
996
|
+
const states = {};
|
|
997
|
+
for (const doc of docs) {
|
|
998
|
+
if (doc.data) {
|
|
999
|
+
states[doc.id] = doc.data;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
onPresence(states);
|
|
1003
|
+
}
|
|
1004
|
+
});
|
|
1005
|
+
this.sendRealtimeMessage({
|
|
1006
|
+
type: "presence_subscribe",
|
|
1007
|
+
request_id: this.generateRequestId(),
|
|
1008
|
+
user_ids: userIds
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* 실시간 연결 상태 확인
|
|
1013
|
+
*/
|
|
1014
|
+
isRealtimeConnected() {
|
|
1015
|
+
return this.realtimeState === "connected";
|
|
1016
|
+
}
|
|
1017
|
+
/**
|
|
1018
|
+
* 실시간 연결 상태 반환
|
|
1019
|
+
*/
|
|
1020
|
+
getRealtimeState() {
|
|
1021
|
+
return this.realtimeState;
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* 상태 변경 콜백 등록
|
|
1025
|
+
*/
|
|
1026
|
+
onRealtimeStateChange(handler) {
|
|
1027
|
+
this.realtimeOnStateChange = handler;
|
|
1028
|
+
return () => {
|
|
1029
|
+
this.realtimeOnStateChange = null;
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
/**
|
|
1033
|
+
* 에러 콜백 등록
|
|
1034
|
+
*/
|
|
1035
|
+
onRealtimeError(handler) {
|
|
1036
|
+
this.realtimeOnError = handler;
|
|
1037
|
+
return () => {
|
|
1038
|
+
this.realtimeOnError = null;
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
// ============ Realtime Private Methods ============
|
|
1042
|
+
setRealtimeState(state) {
|
|
1043
|
+
if (this.realtimeState === state) return;
|
|
1044
|
+
this.realtimeState = state;
|
|
1045
|
+
this.realtimeOnStateChange?.(state);
|
|
1046
|
+
}
|
|
1047
|
+
doRealtimeConnect() {
|
|
1048
|
+
if (!this.realtimeOptions) return Promise.reject(new Error("No realtime options"));
|
|
1049
|
+
this.setRealtimeState("connecting");
|
|
1050
|
+
const baseUrl = this.realtimeOptions.dataServerUrl || this.http.getBaseUrl();
|
|
1051
|
+
const wsUrl = baseUrl.replace(/^http/, "ws");
|
|
1052
|
+
const url = `${wsUrl}/v1/realtime/ws?access_token=${encodeURIComponent(this.realtimeOptions.accessToken)}`;
|
|
1053
|
+
return new Promise((resolve, reject) => {
|
|
1054
|
+
try {
|
|
1055
|
+
this.realtimeWs = new WebSocket(url);
|
|
1056
|
+
let settled = false;
|
|
1057
|
+
const connectTimeout = setTimeout(() => {
|
|
1058
|
+
if (!settled) {
|
|
1059
|
+
settled = true;
|
|
1060
|
+
if (this.realtimeWs) {
|
|
1061
|
+
this.realtimeWs.close();
|
|
1062
|
+
this.realtimeWs = null;
|
|
1063
|
+
}
|
|
1064
|
+
this.setRealtimeState("disconnected");
|
|
1065
|
+
reject(new Error("Connection timeout"));
|
|
1066
|
+
}
|
|
1067
|
+
}, 15e3);
|
|
1068
|
+
this.realtimeWs.onopen = () => {
|
|
1069
|
+
if (!settled) {
|
|
1070
|
+
settled = true;
|
|
1071
|
+
clearTimeout(connectTimeout);
|
|
1072
|
+
}
|
|
1073
|
+
this.setRealtimeState("connected");
|
|
1074
|
+
this.realtimeRetryCount = 0;
|
|
1075
|
+
this.startRealtimePing();
|
|
1076
|
+
this.debugLog("Database realtime connected");
|
|
1077
|
+
this.resubscribeAll();
|
|
1078
|
+
resolve();
|
|
1079
|
+
};
|
|
1080
|
+
this.realtimeWs.onmessage = (event) => {
|
|
1081
|
+
try {
|
|
1082
|
+
const msg = JSON.parse(event.data);
|
|
1083
|
+
this.handleRealtimeMessage(msg);
|
|
1084
|
+
} catch {
|
|
1085
|
+
this.debugLog("Failed to parse realtime message");
|
|
1086
|
+
}
|
|
1087
|
+
};
|
|
1088
|
+
this.realtimeWs.onclose = () => {
|
|
1089
|
+
this.debugLog("Database realtime disconnected");
|
|
1090
|
+
this.realtimeWs = null;
|
|
1091
|
+
this.stopRealtimePing();
|
|
1092
|
+
if (!settled) {
|
|
1093
|
+
settled = true;
|
|
1094
|
+
clearTimeout(connectTimeout);
|
|
1095
|
+
reject(new Error("Connection closed during handshake"));
|
|
1096
|
+
}
|
|
1097
|
+
if (this.realtimeOptions && this.realtimeState !== "disconnected") {
|
|
1098
|
+
this.attemptRealtimeReconnect();
|
|
1099
|
+
}
|
|
1100
|
+
};
|
|
1101
|
+
this.realtimeWs.onerror = () => {
|
|
1102
|
+
this.debugLog("Database realtime error");
|
|
1103
|
+
this.realtimeOnError?.(new Error("WebSocket connection error"));
|
|
1104
|
+
};
|
|
1105
|
+
} catch (e) {
|
|
1106
|
+
this.setRealtimeState("disconnected");
|
|
1107
|
+
reject(e);
|
|
1108
|
+
}
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1111
|
+
/**
|
|
1112
|
+
* 서버에 subscribe 메시지를 보내고 서버 subscription_id를 반환하는 Promise
|
|
1113
|
+
*/
|
|
1114
|
+
sendSubscribeRequest(tableId, handlers, options) {
|
|
1115
|
+
const requestId = this.generateRequestId();
|
|
1116
|
+
this.realtimeHandlers.set(requestId, handlers);
|
|
1117
|
+
const query = options?.where ? {
|
|
1118
|
+
filters: options.where.map((f) => ({
|
|
1119
|
+
field: f.field,
|
|
1120
|
+
operator: f.operator,
|
|
1121
|
+
value: f.value
|
|
1122
|
+
}))
|
|
1123
|
+
} : void 0;
|
|
1124
|
+
this.sendRealtimeMessage({
|
|
1125
|
+
type: "subscribe",
|
|
1126
|
+
request_id: requestId,
|
|
1127
|
+
table_id: tableId,
|
|
1128
|
+
doc_id: options?.docId,
|
|
1129
|
+
query,
|
|
1130
|
+
options: {
|
|
1131
|
+
include_self: options?.includeSelf ?? false,
|
|
1132
|
+
include_metadata_changes: options?.includeMetadataChanges ?? false
|
|
1133
|
+
}
|
|
1134
|
+
});
|
|
1135
|
+
return new Promise((resolve, reject) => {
|
|
1136
|
+
const timeout = setTimeout(() => {
|
|
1137
|
+
this.pendingRequests.delete(requestId);
|
|
1138
|
+
this.realtimeHandlers.delete(requestId);
|
|
1139
|
+
reject(new Error("Subscribe request timeout"));
|
|
1140
|
+
}, 3e4);
|
|
1141
|
+
this.pendingRequests.set(requestId, {
|
|
1142
|
+
resolve: (value) => {
|
|
1143
|
+
const actualSubId = value;
|
|
1144
|
+
const h = this.realtimeHandlers.get(requestId);
|
|
1145
|
+
if (h) {
|
|
1146
|
+
this.realtimeHandlers.delete(requestId);
|
|
1147
|
+
this.realtimeHandlers.set(actualSubId, h);
|
|
1148
|
+
}
|
|
1149
|
+
resolve(actualSubId);
|
|
1150
|
+
},
|
|
1151
|
+
reject,
|
|
1152
|
+
timeout
|
|
1153
|
+
});
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
/**
|
|
1157
|
+
* 재연결 후 모든 활성 구독을 서버에 다시 등록
|
|
1158
|
+
*/
|
|
1159
|
+
resubscribeAll() {
|
|
1160
|
+
if (this.activeSubscriptions.size === 0) return;
|
|
1161
|
+
this.realtimeHandlers.clear();
|
|
1162
|
+
this.pendingRequests.forEach((req) => clearTimeout(req.timeout));
|
|
1163
|
+
this.pendingRequests.clear();
|
|
1164
|
+
this.debugLog(`Resubscribing ${this.activeSubscriptions.size} subscriptions`);
|
|
1165
|
+
for (const [, meta] of this.activeSubscriptions) {
|
|
1166
|
+
this.sendSubscribeRequest(meta.tableId, meta.handlers, meta.options).catch((err) => {
|
|
1167
|
+
meta.handlers.onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
handleRealtimeMessage(msg) {
|
|
1172
|
+
const type = msg.type;
|
|
1173
|
+
switch (type) {
|
|
1174
|
+
case "subscribed": {
|
|
1175
|
+
const requestId = msg.request_id;
|
|
1176
|
+
const subscriptionId = msg.subscription_id;
|
|
1177
|
+
const pending = this.pendingRequests.get(requestId);
|
|
1178
|
+
if (pending) {
|
|
1179
|
+
clearTimeout(pending.timeout);
|
|
1180
|
+
pending.resolve(subscriptionId);
|
|
1181
|
+
this.pendingRequests.delete(requestId);
|
|
1182
|
+
}
|
|
1183
|
+
break;
|
|
1184
|
+
}
|
|
1185
|
+
case "snapshot": {
|
|
1186
|
+
const subId = msg.subscription_id;
|
|
1187
|
+
const handlers = this.realtimeHandlers.get(subId);
|
|
1188
|
+
if (handlers?.onSnapshot) {
|
|
1189
|
+
const docs = msg.docs || [];
|
|
1190
|
+
handlers.onSnapshot(docs, {
|
|
1191
|
+
totalCount: msg.total_count || 0,
|
|
1192
|
+
hasMore: msg.has_more || false
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
break;
|
|
1196
|
+
}
|
|
1197
|
+
case "change": {
|
|
1198
|
+
const subId = msg.subscription_id;
|
|
1199
|
+
const handlers = this.realtimeHandlers.get(subId);
|
|
1200
|
+
if (handlers?.onChange) {
|
|
1201
|
+
const changes = msg.changes || [];
|
|
1202
|
+
handlers.onChange(changes);
|
|
1203
|
+
}
|
|
1204
|
+
break;
|
|
1205
|
+
}
|
|
1206
|
+
case "presence": {
|
|
1207
|
+
const presenceHandlers = this.realtimeHandlers.get("__presence__");
|
|
1208
|
+
if (presenceHandlers?.onSnapshot) {
|
|
1209
|
+
const states = msg.states;
|
|
1210
|
+
const docs = Object.entries(states || {}).map(([userId, state]) => ({
|
|
1211
|
+
id: userId,
|
|
1212
|
+
data: state,
|
|
1213
|
+
exists: true
|
|
1214
|
+
}));
|
|
1215
|
+
presenceHandlers.onSnapshot(docs, { totalCount: docs.length, hasMore: false });
|
|
1216
|
+
}
|
|
1217
|
+
break;
|
|
1218
|
+
}
|
|
1219
|
+
case "error": {
|
|
1220
|
+
const requestId = msg.request_id;
|
|
1221
|
+
const errorMsg = msg.message || "Unknown error";
|
|
1222
|
+
if (requestId) {
|
|
1223
|
+
const pending = this.pendingRequests.get(requestId);
|
|
1224
|
+
if (pending) {
|
|
1225
|
+
clearTimeout(pending.timeout);
|
|
1226
|
+
pending.reject(new Error(errorMsg));
|
|
1227
|
+
this.pendingRequests.delete(requestId);
|
|
1228
|
+
}
|
|
1229
|
+
const handlers = this.realtimeHandlers.get(requestId);
|
|
1230
|
+
if (handlers?.onError) {
|
|
1231
|
+
handlers.onError(new Error(errorMsg));
|
|
1232
|
+
}
|
|
1233
|
+
} else {
|
|
1234
|
+
this.realtimeOnError?.(new Error(errorMsg));
|
|
1235
|
+
}
|
|
1236
|
+
break;
|
|
1237
|
+
}
|
|
1238
|
+
case "pong":
|
|
1239
|
+
break;
|
|
1240
|
+
case "unsubscribed":
|
|
1241
|
+
case "presence_set_ack":
|
|
1242
|
+
case "presence_subscribed":
|
|
1243
|
+
case "typing_subscribed":
|
|
1244
|
+
break;
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
attemptRealtimeReconnect() {
|
|
1248
|
+
const maxRetries = this.realtimeOptions?.maxRetries ?? 5;
|
|
1249
|
+
const retryInterval = this.realtimeOptions?.retryInterval ?? 1e3;
|
|
1250
|
+
if (this.realtimeRetryCount >= maxRetries) {
|
|
1251
|
+
this.setRealtimeState("disconnected");
|
|
1252
|
+
this.realtimeOnError?.(new Error("Realtime connection lost. Max retries exceeded."));
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
this.setRealtimeState("connecting");
|
|
1256
|
+
this.realtimeRetryCount++;
|
|
1257
|
+
const delay = Math.min(retryInterval * Math.pow(2, this.realtimeRetryCount - 1), 3e4);
|
|
1258
|
+
this.debugLog(`Reconnecting in ${delay}ms (attempt ${this.realtimeRetryCount}/${maxRetries})`);
|
|
1259
|
+
setTimeout(() => {
|
|
1260
|
+
if (this.realtimeOptions) {
|
|
1261
|
+
this.doRealtimeConnect().catch((err) => {
|
|
1262
|
+
this.debugLog(`Reconnect failed: ${err}`);
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
}, delay);
|
|
1266
|
+
}
|
|
1267
|
+
startRealtimePing() {
|
|
1268
|
+
this.stopRealtimePing();
|
|
1269
|
+
this.pingInterval = setInterval(() => {
|
|
1270
|
+
if (this.realtimeState === "connected" && this.realtimeWs?.readyState === WebSocket.OPEN) {
|
|
1271
|
+
this.sendRealtimeMessage({ type: "ping", timestamp: Date.now() });
|
|
1272
|
+
}
|
|
1273
|
+
}, 3e4);
|
|
1274
|
+
}
|
|
1275
|
+
stopRealtimePing() {
|
|
1276
|
+
if (this.pingInterval) {
|
|
1277
|
+
clearInterval(this.pingInterval);
|
|
1278
|
+
this.pingInterval = null;
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
sendRealtimeMessage(msg) {
|
|
1282
|
+
if (this.realtimeWs?.readyState === WebSocket.OPEN) {
|
|
1283
|
+
this.realtimeWs.send(JSON.stringify(msg));
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
generateRequestId() {
|
|
1287
|
+
return "req_" + Date.now() + "_" + Math.random().toString(36).substring(2, 9);
|
|
1288
|
+
}
|
|
1289
|
+
debugLog(message) {
|
|
1290
|
+
if (this.realtimeOptions?.debug) {
|
|
1291
|
+
console.log(`[DatabaseRealtime] ${message}`);
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
864
1294
|
};
|
|
865
1295
|
|
|
866
1296
|
// src/api/storage.ts
|
|
@@ -1307,7 +1737,9 @@ var RealtimeAPI = class {
|
|
|
1307
1737
|
maxRetries: 5,
|
|
1308
1738
|
retryInterval: 1e3,
|
|
1309
1739
|
userId: "",
|
|
1310
|
-
accessToken: ""
|
|
1740
|
+
accessToken: "",
|
|
1741
|
+
timeout: 3e4,
|
|
1742
|
+
debug: false
|
|
1311
1743
|
};
|
|
1312
1744
|
this.retryCount = 0;
|
|
1313
1745
|
this.pendingRequests = /* @__PURE__ */ new Map();
|
|
@@ -1545,6 +1977,12 @@ var RealtimeAPI = class {
|
|
|
1545
1977
|
getState() {
|
|
1546
1978
|
return this.state;
|
|
1547
1979
|
}
|
|
1980
|
+
/**
|
|
1981
|
+
* 연결 여부 확인
|
|
1982
|
+
*/
|
|
1983
|
+
isConnected() {
|
|
1984
|
+
return this.state === "connected";
|
|
1985
|
+
}
|
|
1548
1986
|
/**
|
|
1549
1987
|
* 상태 변경 핸들러 등록
|
|
1550
1988
|
*/
|
|
@@ -1570,53 +2008,97 @@ var RealtimeAPI = class {
|
|
|
1570
2008
|
return new Promise((resolve, reject) => {
|
|
1571
2009
|
this.state = "connecting";
|
|
1572
2010
|
this.notifyStateChange();
|
|
2011
|
+
this.log("Connecting...");
|
|
1573
2012
|
const wsUrl = this.socketUrl.replace(/^http/, "ws");
|
|
1574
2013
|
let url;
|
|
1575
2014
|
if (this.options.accessToken) {
|
|
1576
2015
|
url = `${wsUrl}/v1/realtime/auth?access_token=${encodeURIComponent(this.options.accessToken)}&client_id=${this.clientId}`;
|
|
2016
|
+
this.log("Using accessToken authentication");
|
|
1577
2017
|
} else {
|
|
1578
2018
|
const apiKey = this.http.getApiKey();
|
|
1579
2019
|
if (!apiKey) {
|
|
1580
|
-
|
|
2020
|
+
const error = new Error("API Key or accessToken is required for realtime connection");
|
|
2021
|
+
this.log("Connection failed: no API Key or accessToken");
|
|
2022
|
+
reject(error);
|
|
1581
2023
|
return;
|
|
1582
2024
|
}
|
|
1583
2025
|
url = `${wsUrl}/v1/realtime/auth?api_key=${encodeURIComponent(apiKey)}&client_id=${this.clientId}`;
|
|
2026
|
+
this.log("Using API Key authentication");
|
|
1584
2027
|
}
|
|
1585
2028
|
if (this.userId) {
|
|
1586
2029
|
url += `&user_id=${encodeURIComponent(this.userId)}`;
|
|
1587
2030
|
}
|
|
2031
|
+
let settled = false;
|
|
2032
|
+
const timeoutId = setTimeout(() => {
|
|
2033
|
+
if (!settled) {
|
|
2034
|
+
settled = true;
|
|
2035
|
+
this.log(`Connection timeout after ${this.options.timeout}ms`);
|
|
2036
|
+
if (this.ws) {
|
|
2037
|
+
this.ws.close();
|
|
2038
|
+
this.ws = null;
|
|
2039
|
+
}
|
|
2040
|
+
this.state = "disconnected";
|
|
2041
|
+
this.notifyStateChange();
|
|
2042
|
+
reject(new Error(`Connection timeout after ${this.options.timeout}ms`));
|
|
2043
|
+
}
|
|
2044
|
+
}, this.options.timeout);
|
|
1588
2045
|
try {
|
|
2046
|
+
this.log(`Connecting to ${wsUrl}`);
|
|
1589
2047
|
this.ws = new WebSocket(url);
|
|
1590
2048
|
this.ws.onopen = () => {
|
|
2049
|
+
this.log("WebSocket opened, waiting for connected event...");
|
|
1591
2050
|
};
|
|
1592
2051
|
this.ws.onmessage = (event) => {
|
|
1593
2052
|
const messages = event.data.split("\n").filter((line) => line.trim());
|
|
1594
2053
|
for (const line of messages) {
|
|
1595
2054
|
try {
|
|
1596
2055
|
const msg = JSON.parse(line);
|
|
1597
|
-
this.handleServerMessage(msg,
|
|
2056
|
+
this.handleServerMessage(msg, () => {
|
|
2057
|
+
if (!settled) {
|
|
2058
|
+
settled = true;
|
|
2059
|
+
clearTimeout(timeoutId);
|
|
2060
|
+
this.log("Connected successfully");
|
|
2061
|
+
resolve();
|
|
2062
|
+
}
|
|
2063
|
+
});
|
|
1598
2064
|
} catch (e) {
|
|
1599
2065
|
console.error("[Realtime] Failed to parse message:", line, e);
|
|
1600
2066
|
}
|
|
1601
2067
|
}
|
|
1602
2068
|
};
|
|
1603
|
-
this.ws.onclose = () => {
|
|
2069
|
+
this.ws.onclose = (event) => {
|
|
2070
|
+
this.log(`WebSocket closed: code=${event.code}, reason=${event.reason}`);
|
|
2071
|
+
if (!settled && this.state === "connecting") {
|
|
2072
|
+
settled = true;
|
|
2073
|
+
clearTimeout(timeoutId);
|
|
2074
|
+
reject(new Error(`Connection closed: ${event.reason || "unknown reason"}`));
|
|
2075
|
+
}
|
|
1604
2076
|
if (this.state === "connected" || this.state === "connecting") {
|
|
1605
2077
|
this.handleDisconnect();
|
|
1606
2078
|
}
|
|
1607
2079
|
};
|
|
1608
2080
|
this.ws.onerror = (error) => {
|
|
2081
|
+
this.log("WebSocket error occurred");
|
|
1609
2082
|
console.error("[Realtime] WebSocket error:", error);
|
|
1610
2083
|
this.notifyError(new Error("WebSocket connection error"));
|
|
1611
|
-
if (this.state === "connecting") {
|
|
2084
|
+
if (!settled && this.state === "connecting") {
|
|
2085
|
+
settled = true;
|
|
2086
|
+
clearTimeout(timeoutId);
|
|
1612
2087
|
reject(new Error("Failed to connect"));
|
|
1613
2088
|
}
|
|
1614
2089
|
};
|
|
1615
2090
|
} catch (e) {
|
|
2091
|
+
settled = true;
|
|
2092
|
+
clearTimeout(timeoutId);
|
|
1616
2093
|
reject(e);
|
|
1617
2094
|
}
|
|
1618
2095
|
});
|
|
1619
2096
|
}
|
|
2097
|
+
log(message) {
|
|
2098
|
+
if (this.options.debug) {
|
|
2099
|
+
console.log(`[Realtime] ${message}`);
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
1620
2102
|
handleServerMessage(msg, connectResolve) {
|
|
1621
2103
|
switch (msg.event) {
|
|
1622
2104
|
case "connected": {
|