connectbase-client 0.6.29 → 0.6.30
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 +207 -5
- package/dist/index.d.ts +207 -5
- package/dist/index.js +548 -22
- package/dist/index.mjs +548 -22
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1760,6 +1760,15 @@ var RealtimeAPI = class {
|
|
|
1760
1760
|
this.streamSessions = /* @__PURE__ */ new Map();
|
|
1761
1761
|
this.stateHandlers = [];
|
|
1762
1762
|
this.errorHandlers = [];
|
|
1763
|
+
// Presence handlers
|
|
1764
|
+
this.presenceHandlers = [];
|
|
1765
|
+
this.presenceSubscriptions = /* @__PURE__ */ new Map();
|
|
1766
|
+
// Typing handlers
|
|
1767
|
+
this.typingHandlers = /* @__PURE__ */ new Map();
|
|
1768
|
+
// Read receipt handlers
|
|
1769
|
+
this.readReceiptHandlers = /* @__PURE__ */ new Map();
|
|
1770
|
+
// 연결 중일 때 대기하는 Promise들
|
|
1771
|
+
this.connectPromise = null;
|
|
1763
1772
|
this.http = http;
|
|
1764
1773
|
this.socketUrl = socketUrl;
|
|
1765
1774
|
this.clientId = this.generateClientId();
|
|
@@ -1779,14 +1788,22 @@ var RealtimeAPI = class {
|
|
|
1779
1788
|
* - userId: 사용자 식별자 (표시용)
|
|
1780
1789
|
*/
|
|
1781
1790
|
async connect(options = {}) {
|
|
1782
|
-
if (this.state === "connected"
|
|
1791
|
+
if (this.state === "connected") {
|
|
1783
1792
|
return;
|
|
1784
1793
|
}
|
|
1794
|
+
if (this.state === "connecting" && this.connectPromise) {
|
|
1795
|
+
return this.connectPromise;
|
|
1796
|
+
}
|
|
1785
1797
|
this.options = { ...this.options, ...options };
|
|
1786
1798
|
if (options.userId) {
|
|
1787
1799
|
this.userId = options.userId;
|
|
1788
1800
|
}
|
|
1789
|
-
|
|
1801
|
+
this.connectPromise = this.doConnect();
|
|
1802
|
+
try {
|
|
1803
|
+
await this.connectPromise;
|
|
1804
|
+
} finally {
|
|
1805
|
+
this.connectPromise = null;
|
|
1806
|
+
}
|
|
1790
1807
|
}
|
|
1791
1808
|
/**
|
|
1792
1809
|
* 연결 해제
|
|
@@ -1804,6 +1821,20 @@ var RealtimeAPI = class {
|
|
|
1804
1821
|
});
|
|
1805
1822
|
this.pendingRequests.clear();
|
|
1806
1823
|
this.subscriptions.clear();
|
|
1824
|
+
this.streamSessions.forEach((session) => {
|
|
1825
|
+
if (session.handlers.onError) {
|
|
1826
|
+
session.handlers.onError(new Error("Connection closed"));
|
|
1827
|
+
}
|
|
1828
|
+
});
|
|
1829
|
+
this.streamSessions.clear();
|
|
1830
|
+
this.presenceHandlers = [];
|
|
1831
|
+
this.presenceSubscriptions.clear();
|
|
1832
|
+
this.typingHandlers.clear();
|
|
1833
|
+
this.readReceiptHandlers.clear();
|
|
1834
|
+
this._connectionId = null;
|
|
1835
|
+
this._appId = null;
|
|
1836
|
+
this.retryCount = 0;
|
|
1837
|
+
this.connectPromise = null;
|
|
1807
1838
|
}
|
|
1808
1839
|
/**
|
|
1809
1840
|
* 카테고리 구독
|
|
@@ -1839,6 +1870,10 @@ var RealtimeAPI = class {
|
|
|
1839
1870
|
},
|
|
1840
1871
|
onMessage: (handler) => {
|
|
1841
1872
|
handlers.push(handler);
|
|
1873
|
+
return () => {
|
|
1874
|
+
const idx = handlers.indexOf(handler);
|
|
1875
|
+
if (idx > -1) handlers.splice(idx, 1);
|
|
1876
|
+
};
|
|
1842
1877
|
}
|
|
1843
1878
|
};
|
|
1844
1879
|
return subscription;
|
|
@@ -1946,21 +1981,26 @@ var RealtimeAPI = class {
|
|
|
1946
1981
|
handlers,
|
|
1947
1982
|
requestId
|
|
1948
1983
|
});
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1984
|
+
try {
|
|
1985
|
+
this.sendRaw({
|
|
1986
|
+
category: "",
|
|
1987
|
+
action: "stream",
|
|
1988
|
+
data: {
|
|
1989
|
+
provider: options.provider,
|
|
1990
|
+
model: options.model,
|
|
1991
|
+
messages,
|
|
1992
|
+
system: options.system,
|
|
1993
|
+
temperature: options.temperature,
|
|
1994
|
+
max_tokens: options.maxTokens,
|
|
1995
|
+
session_id: sessionId,
|
|
1996
|
+
metadata: options.metadata
|
|
1997
|
+
},
|
|
1998
|
+
request_id: requestId
|
|
1999
|
+
});
|
|
2000
|
+
} catch (e) {
|
|
2001
|
+
this.streamSessions.delete(sessionId);
|
|
2002
|
+
throw e;
|
|
2003
|
+
}
|
|
1964
2004
|
return {
|
|
1965
2005
|
sessionId,
|
|
1966
2006
|
stop: async () => {
|
|
@@ -2016,6 +2056,344 @@ var RealtimeAPI = class {
|
|
|
2016
2056
|
if (idx > -1) this.errorHandlers.splice(idx, 1);
|
|
2017
2057
|
};
|
|
2018
2058
|
}
|
|
2059
|
+
// ========== Presence API ==========
|
|
2060
|
+
/**
|
|
2061
|
+
* 프레전스 상태 설정
|
|
2062
|
+
* @param status 상태 (online, away, busy, offline)
|
|
2063
|
+
* @param options 추가 옵션 (device, metadata)
|
|
2064
|
+
*
|
|
2065
|
+
* @example
|
|
2066
|
+
* ```typescript
|
|
2067
|
+
* await client.realtime.setPresence('online', {
|
|
2068
|
+
* device: 'web',
|
|
2069
|
+
* metadata: { nickname: 'John' }
|
|
2070
|
+
* })
|
|
2071
|
+
* ```
|
|
2072
|
+
*/
|
|
2073
|
+
async setPresence(status, options = {}) {
|
|
2074
|
+
if (this.state !== "connected") {
|
|
2075
|
+
throw new Error("Not connected");
|
|
2076
|
+
}
|
|
2077
|
+
const requestId = this.generateRequestId();
|
|
2078
|
+
await this.sendRequest({
|
|
2079
|
+
category: "",
|
|
2080
|
+
action: "presence_set",
|
|
2081
|
+
data: {
|
|
2082
|
+
status,
|
|
2083
|
+
device: options.device,
|
|
2084
|
+
metadata: options.metadata
|
|
2085
|
+
},
|
|
2086
|
+
request_id: requestId
|
|
2087
|
+
});
|
|
2088
|
+
}
|
|
2089
|
+
/**
|
|
2090
|
+
* 연결 해제 시 자동으로 설정될 프레전스 상태 지정
|
|
2091
|
+
* @param status 연결 해제 시 변경할 상태
|
|
2092
|
+
* @param metadata 연결 해제 시 업데이트할 메타데이터
|
|
2093
|
+
*
|
|
2094
|
+
* @example
|
|
2095
|
+
* ```typescript
|
|
2096
|
+
* // 연결이 끊기면 자동으로 offline으로 변경
|
|
2097
|
+
* await client.realtime.setPresenceOnDisconnect('offline')
|
|
2098
|
+
* ```
|
|
2099
|
+
*/
|
|
2100
|
+
async setPresenceOnDisconnect(status, metadata) {
|
|
2101
|
+
if (this.state !== "connected") {
|
|
2102
|
+
throw new Error("Not connected");
|
|
2103
|
+
}
|
|
2104
|
+
const requestId = this.generateRequestId();
|
|
2105
|
+
await this.sendRequest({
|
|
2106
|
+
category: "",
|
|
2107
|
+
action: "presence_on_disconnect",
|
|
2108
|
+
data: { status, metadata },
|
|
2109
|
+
request_id: requestId
|
|
2110
|
+
});
|
|
2111
|
+
}
|
|
2112
|
+
/**
|
|
2113
|
+
* 특정 사용자의 프레전스 조회
|
|
2114
|
+
* @param userId 조회할 사용자 ID
|
|
2115
|
+
* @returns 프레전스 정보
|
|
2116
|
+
*
|
|
2117
|
+
* @example
|
|
2118
|
+
* ```typescript
|
|
2119
|
+
* const presence = await client.realtime.getPresence('user-123')
|
|
2120
|
+
* console.log(presence.status) // 'online'
|
|
2121
|
+
* ```
|
|
2122
|
+
*/
|
|
2123
|
+
async getPresence(userId) {
|
|
2124
|
+
if (this.state !== "connected") {
|
|
2125
|
+
throw new Error("Not connected");
|
|
2126
|
+
}
|
|
2127
|
+
const requestId = this.generateRequestId();
|
|
2128
|
+
const response = await this.sendRequest({
|
|
2129
|
+
category: "",
|
|
2130
|
+
action: "presence_get",
|
|
2131
|
+
data: { user_id: userId },
|
|
2132
|
+
request_id: requestId
|
|
2133
|
+
});
|
|
2134
|
+
return {
|
|
2135
|
+
userId: response.user_id,
|
|
2136
|
+
status: response.status,
|
|
2137
|
+
lastSeen: response.last_seen,
|
|
2138
|
+
device: response.device,
|
|
2139
|
+
metadata: response.metadata
|
|
2140
|
+
};
|
|
2141
|
+
}
|
|
2142
|
+
/**
|
|
2143
|
+
* 여러 사용자의 프레전스 조회
|
|
2144
|
+
* @param userIds 조회할 사용자 ID 목록
|
|
2145
|
+
* @returns 사용자별 프레전스 정보
|
|
2146
|
+
*
|
|
2147
|
+
* @example
|
|
2148
|
+
* ```typescript
|
|
2149
|
+
* const result = await client.realtime.getPresenceMany(['user-1', 'user-2'])
|
|
2150
|
+
* console.log(result.users['user-1'].status)
|
|
2151
|
+
* ```
|
|
2152
|
+
*/
|
|
2153
|
+
async getPresenceMany(userIds) {
|
|
2154
|
+
if (this.state !== "connected") {
|
|
2155
|
+
throw new Error("Not connected");
|
|
2156
|
+
}
|
|
2157
|
+
const requestId = this.generateRequestId();
|
|
2158
|
+
const response = await this.sendRequest({
|
|
2159
|
+
category: "",
|
|
2160
|
+
action: "presence_get_many",
|
|
2161
|
+
data: { user_ids: userIds },
|
|
2162
|
+
request_id: requestId
|
|
2163
|
+
});
|
|
2164
|
+
const users = {};
|
|
2165
|
+
for (const [id, data] of Object.entries(response.users)) {
|
|
2166
|
+
users[id] = {
|
|
2167
|
+
userId: data.user_id,
|
|
2168
|
+
status: data.status,
|
|
2169
|
+
lastSeen: data.last_seen,
|
|
2170
|
+
device: data.device,
|
|
2171
|
+
metadata: data.metadata
|
|
2172
|
+
};
|
|
2173
|
+
}
|
|
2174
|
+
return { users };
|
|
2175
|
+
}
|
|
2176
|
+
/**
|
|
2177
|
+
* 특정 사용자의 프레전스 변경 구독
|
|
2178
|
+
* @param userId 구독할 사용자 ID
|
|
2179
|
+
* @param handler 프레전스 변경 핸들러
|
|
2180
|
+
* @returns 구독 해제 함수
|
|
2181
|
+
*
|
|
2182
|
+
* @example
|
|
2183
|
+
* ```typescript
|
|
2184
|
+
* const unsubscribe = await client.realtime.subscribePresence('user-123', (presence) => {
|
|
2185
|
+
* console.log(`${presence.userId} is now ${presence.status}`)
|
|
2186
|
+
* })
|
|
2187
|
+
*
|
|
2188
|
+
* // 나중에 구독 해제
|
|
2189
|
+
* unsubscribe()
|
|
2190
|
+
* ```
|
|
2191
|
+
*/
|
|
2192
|
+
async subscribePresence(userId, handler) {
|
|
2193
|
+
if (this.state !== "connected") {
|
|
2194
|
+
throw new Error("Not connected");
|
|
2195
|
+
}
|
|
2196
|
+
if (!this.presenceSubscriptions.has(userId)) {
|
|
2197
|
+
const requestId = this.generateRequestId();
|
|
2198
|
+
await this.sendRequest({
|
|
2199
|
+
category: "",
|
|
2200
|
+
action: "presence_subscribe",
|
|
2201
|
+
data: { user_id: userId },
|
|
2202
|
+
request_id: requestId
|
|
2203
|
+
});
|
|
2204
|
+
this.presenceSubscriptions.set(userId, []);
|
|
2205
|
+
}
|
|
2206
|
+
const handlers = this.presenceSubscriptions.get(userId);
|
|
2207
|
+
handlers.push(handler);
|
|
2208
|
+
return () => {
|
|
2209
|
+
const idx = handlers.indexOf(handler);
|
|
2210
|
+
if (idx > -1) handlers.splice(idx, 1);
|
|
2211
|
+
if (handlers.length === 0) {
|
|
2212
|
+
this.presenceSubscriptions.delete(userId);
|
|
2213
|
+
if (this.state === "connected") {
|
|
2214
|
+
const requestId = this.generateRequestId();
|
|
2215
|
+
this.sendRequest({
|
|
2216
|
+
category: "",
|
|
2217
|
+
action: "presence_unsubscribe",
|
|
2218
|
+
data: { user_id: userId },
|
|
2219
|
+
request_id: requestId
|
|
2220
|
+
}).catch((e) => {
|
|
2221
|
+
this.log(`Failed to unsubscribe presence for ${userId}: ${e}`);
|
|
2222
|
+
});
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
};
|
|
2226
|
+
}
|
|
2227
|
+
/**
|
|
2228
|
+
* 전체 프레전스 변경 이벤트 구독
|
|
2229
|
+
* @param handler 프레전스 변경 핸들러
|
|
2230
|
+
* @returns 구독 해제 함수
|
|
2231
|
+
*
|
|
2232
|
+
* @example
|
|
2233
|
+
* ```typescript
|
|
2234
|
+
* const unsubscribe = client.realtime.onPresenceChange((presence) => {
|
|
2235
|
+
* if (presence.eventType === 'join') {
|
|
2236
|
+
* console.log(`${presence.userId} joined`)
|
|
2237
|
+
* }
|
|
2238
|
+
* })
|
|
2239
|
+
* ```
|
|
2240
|
+
*/
|
|
2241
|
+
onPresenceChange(handler) {
|
|
2242
|
+
this.presenceHandlers.push(handler);
|
|
2243
|
+
return () => {
|
|
2244
|
+
const idx = this.presenceHandlers.indexOf(handler);
|
|
2245
|
+
if (idx > -1) this.presenceHandlers.splice(idx, 1);
|
|
2246
|
+
};
|
|
2247
|
+
}
|
|
2248
|
+
// ========== Typing API ==========
|
|
2249
|
+
/**
|
|
2250
|
+
* 타이핑 시작 알림
|
|
2251
|
+
* @param roomId 타이핑 중인 룸/채널 ID (category 이름)
|
|
2252
|
+
*
|
|
2253
|
+
* @example
|
|
2254
|
+
* ```typescript
|
|
2255
|
+
* // 입력 시작 시
|
|
2256
|
+
* input.addEventListener('focus', () => {
|
|
2257
|
+
* client.realtime.startTyping('chat-room-1')
|
|
2258
|
+
* })
|
|
2259
|
+
* ```
|
|
2260
|
+
*/
|
|
2261
|
+
async startTyping(roomId) {
|
|
2262
|
+
if (this.state !== "connected") {
|
|
2263
|
+
throw new Error("Not connected");
|
|
2264
|
+
}
|
|
2265
|
+
const requestId = this.generateRequestId();
|
|
2266
|
+
await this.sendRequest({
|
|
2267
|
+
category: "",
|
|
2268
|
+
action: "typing_start",
|
|
2269
|
+
data: { room_id: roomId },
|
|
2270
|
+
request_id: requestId
|
|
2271
|
+
});
|
|
2272
|
+
}
|
|
2273
|
+
/**
|
|
2274
|
+
* 타이핑 종료 알림
|
|
2275
|
+
* @param roomId 타이핑을 종료할 룸/채널 ID
|
|
2276
|
+
*
|
|
2277
|
+
* @example
|
|
2278
|
+
* ```typescript
|
|
2279
|
+
* // 입력 종료 또는 메시지 전송 시
|
|
2280
|
+
* input.addEventListener('blur', () => {
|
|
2281
|
+
* client.realtime.stopTyping('chat-room-1')
|
|
2282
|
+
* })
|
|
2283
|
+
* ```
|
|
2284
|
+
*/
|
|
2285
|
+
async stopTyping(roomId) {
|
|
2286
|
+
if (this.state !== "connected") {
|
|
2287
|
+
throw new Error("Not connected");
|
|
2288
|
+
}
|
|
2289
|
+
const requestId = this.generateRequestId();
|
|
2290
|
+
await this.sendRequest({
|
|
2291
|
+
category: "",
|
|
2292
|
+
action: "typing_stop",
|
|
2293
|
+
data: { room_id: roomId },
|
|
2294
|
+
request_id: requestId
|
|
2295
|
+
});
|
|
2296
|
+
}
|
|
2297
|
+
/**
|
|
2298
|
+
* 타이핑 상태 변경 구독
|
|
2299
|
+
* @param roomId 구독할 룸/채널 ID
|
|
2300
|
+
* @param handler 타이핑 상태 변경 핸들러
|
|
2301
|
+
* @returns 구독 해제 함수
|
|
2302
|
+
*
|
|
2303
|
+
* @example
|
|
2304
|
+
* ```typescript
|
|
2305
|
+
* const unsubscribe = await client.realtime.onTypingChange('chat-room-1', (typing) => {
|
|
2306
|
+
* if (typing.users.length > 0) {
|
|
2307
|
+
* indicator.textContent = `${typing.users.join(', ')} is typing...`
|
|
2308
|
+
* } else {
|
|
2309
|
+
* indicator.textContent = ''
|
|
2310
|
+
* }
|
|
2311
|
+
* })
|
|
2312
|
+
* ```
|
|
2313
|
+
*/
|
|
2314
|
+
async onTypingChange(roomId, handler) {
|
|
2315
|
+
if (this.state !== "connected") {
|
|
2316
|
+
throw new Error("Not connected");
|
|
2317
|
+
}
|
|
2318
|
+
if (!this.typingHandlers.has(roomId)) {
|
|
2319
|
+
const requestId = this.generateRequestId();
|
|
2320
|
+
await this.sendRequest({
|
|
2321
|
+
category: "",
|
|
2322
|
+
action: "typing_subscribe",
|
|
2323
|
+
data: { room_id: roomId },
|
|
2324
|
+
request_id: requestId
|
|
2325
|
+
});
|
|
2326
|
+
this.typingHandlers.set(roomId, []);
|
|
2327
|
+
}
|
|
2328
|
+
const handlers = this.typingHandlers.get(roomId);
|
|
2329
|
+
handlers.push(handler);
|
|
2330
|
+
return () => {
|
|
2331
|
+
const idx = handlers.indexOf(handler);
|
|
2332
|
+
if (idx > -1) handlers.splice(idx, 1);
|
|
2333
|
+
if (handlers.length === 0) {
|
|
2334
|
+
this.typingHandlers.delete(roomId);
|
|
2335
|
+
if (this.state === "connected") {
|
|
2336
|
+
const requestId = this.generateRequestId();
|
|
2337
|
+
this.sendRequest({
|
|
2338
|
+
category: "",
|
|
2339
|
+
action: "typing_unsubscribe",
|
|
2340
|
+
data: { room_id: roomId },
|
|
2341
|
+
request_id: requestId
|
|
2342
|
+
}).catch((e) => {
|
|
2343
|
+
this.log(`Failed to unsubscribe typing for ${roomId}: ${e}`);
|
|
2344
|
+
});
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
};
|
|
2348
|
+
}
|
|
2349
|
+
// ========== Read Receipt API ==========
|
|
2350
|
+
/**
|
|
2351
|
+
* 메시지 읽음 표시
|
|
2352
|
+
* @param category 카테고리 이름
|
|
2353
|
+
* @param messageIds 읽음 처리할 메시지 ID 목록
|
|
2354
|
+
*
|
|
2355
|
+
* @example
|
|
2356
|
+
* ```typescript
|
|
2357
|
+
* // 메시지를 읽었을 때
|
|
2358
|
+
* await client.realtime.markRead('chat-room-1', ['msg-1', 'msg-2'])
|
|
2359
|
+
* ```
|
|
2360
|
+
*/
|
|
2361
|
+
async markRead(category, messageIds) {
|
|
2362
|
+
if (this.state !== "connected") {
|
|
2363
|
+
throw new Error("Not connected");
|
|
2364
|
+
}
|
|
2365
|
+
const requestId = this.generateRequestId();
|
|
2366
|
+
await this.sendRequest({
|
|
2367
|
+
category,
|
|
2368
|
+
action: "mark_read",
|
|
2369
|
+
data: { message_ids: messageIds },
|
|
2370
|
+
request_id: requestId
|
|
2371
|
+
});
|
|
2372
|
+
}
|
|
2373
|
+
/**
|
|
2374
|
+
* 읽음 확인 이벤트 구독
|
|
2375
|
+
* @param category 구독할 카테고리 이름
|
|
2376
|
+
* @param handler 읽음 확인 핸들러
|
|
2377
|
+
* @returns 구독 해제 함수
|
|
2378
|
+
*
|
|
2379
|
+
* @example
|
|
2380
|
+
* ```typescript
|
|
2381
|
+
* const unsubscribe = client.realtime.onReadReceipt('chat-room-1', (receipt) => {
|
|
2382
|
+
* console.log(`${receipt.readerId} read messages: ${receipt.messageIds.join(', ')}`)
|
|
2383
|
+
* })
|
|
2384
|
+
* ```
|
|
2385
|
+
*/
|
|
2386
|
+
onReadReceipt(category, handler) {
|
|
2387
|
+
if (!this.readReceiptHandlers.has(category)) {
|
|
2388
|
+
this.readReceiptHandlers.set(category, []);
|
|
2389
|
+
}
|
|
2390
|
+
const handlers = this.readReceiptHandlers.get(category);
|
|
2391
|
+
handlers.push(handler);
|
|
2392
|
+
return () => {
|
|
2393
|
+
const idx = handlers.indexOf(handler);
|
|
2394
|
+
if (idx > -1) handlers.splice(idx, 1);
|
|
2395
|
+
};
|
|
2396
|
+
}
|
|
2019
2397
|
// Private methods
|
|
2020
2398
|
async doConnect() {
|
|
2021
2399
|
return new Promise((resolve, reject) => {
|
|
@@ -2207,26 +2585,174 @@ var RealtimeAPI = class {
|
|
|
2207
2585
|
}
|
|
2208
2586
|
break;
|
|
2209
2587
|
}
|
|
2588
|
+
// Presence events
|
|
2589
|
+
case "presence":
|
|
2590
|
+
case "presence_status": {
|
|
2591
|
+
const data = msg.data;
|
|
2592
|
+
const presence = {
|
|
2593
|
+
userId: data.user_id,
|
|
2594
|
+
status: data.status,
|
|
2595
|
+
lastSeen: data.last_seen,
|
|
2596
|
+
device: data.device,
|
|
2597
|
+
metadata: data.metadata,
|
|
2598
|
+
eventType: data.event_type
|
|
2599
|
+
};
|
|
2600
|
+
this.presenceHandlers.forEach((h) => h(presence));
|
|
2601
|
+
const userHandlers = this.presenceSubscriptions.get(data.user_id);
|
|
2602
|
+
if (userHandlers) {
|
|
2603
|
+
userHandlers.forEach((h) => h(presence));
|
|
2604
|
+
}
|
|
2605
|
+
if (msg.request_id) {
|
|
2606
|
+
const pending = this.pendingRequests.get(msg.request_id);
|
|
2607
|
+
if (pending) {
|
|
2608
|
+
clearTimeout(pending.timeout);
|
|
2609
|
+
pending.resolve(msg.data);
|
|
2610
|
+
this.pendingRequests.delete(msg.request_id);
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
break;
|
|
2614
|
+
}
|
|
2615
|
+
// Typing events
|
|
2616
|
+
case "typing": {
|
|
2617
|
+
const data = msg.data;
|
|
2618
|
+
const typing = {
|
|
2619
|
+
roomId: data.room_id,
|
|
2620
|
+
users: data.users
|
|
2621
|
+
};
|
|
2622
|
+
const typingHandlers = this.typingHandlers.get(data.room_id);
|
|
2623
|
+
if (typingHandlers) {
|
|
2624
|
+
typingHandlers.forEach((h) => h(typing));
|
|
2625
|
+
}
|
|
2626
|
+
break;
|
|
2627
|
+
}
|
|
2628
|
+
// Read receipt events
|
|
2629
|
+
case "read_receipt": {
|
|
2630
|
+
const data = msg.data;
|
|
2631
|
+
const receipt = {
|
|
2632
|
+
category: data.category,
|
|
2633
|
+
messageIds: data.message_ids,
|
|
2634
|
+
readerId: data.reader_id,
|
|
2635
|
+
readAt: data.read_at
|
|
2636
|
+
};
|
|
2637
|
+
const receiptHandlers = this.readReceiptHandlers.get(data.category);
|
|
2638
|
+
if (receiptHandlers) {
|
|
2639
|
+
receiptHandlers.forEach((h) => h(receipt));
|
|
2640
|
+
}
|
|
2641
|
+
break;
|
|
2642
|
+
}
|
|
2210
2643
|
}
|
|
2211
2644
|
}
|
|
2212
2645
|
handleDisconnect() {
|
|
2213
2646
|
this.ws = null;
|
|
2214
2647
|
this._connectionId = null;
|
|
2648
|
+
this.streamSessions.forEach((session) => {
|
|
2649
|
+
if (session.handlers.onError) {
|
|
2650
|
+
session.handlers.onError(new Error("Connection lost"));
|
|
2651
|
+
}
|
|
2652
|
+
});
|
|
2653
|
+
this.streamSessions.clear();
|
|
2654
|
+
const previousSubscriptions = /* @__PURE__ */ new Map();
|
|
2655
|
+
for (const [category, sub] of this.subscriptions) {
|
|
2656
|
+
previousSubscriptions.set(category, [...sub.handlers]);
|
|
2657
|
+
}
|
|
2658
|
+
this.subscriptions.clear();
|
|
2659
|
+
const previousPresenceSubscriptions = /* @__PURE__ */ new Map();
|
|
2660
|
+
for (const [userId, handlers] of this.presenceSubscriptions) {
|
|
2661
|
+
previousPresenceSubscriptions.set(userId, [...handlers]);
|
|
2662
|
+
}
|
|
2663
|
+
this.presenceSubscriptions.clear();
|
|
2664
|
+
const previousTypingHandlers = /* @__PURE__ */ new Map();
|
|
2665
|
+
for (const [roomId, handlers] of this.typingHandlers) {
|
|
2666
|
+
previousTypingHandlers.set(roomId, [...handlers]);
|
|
2667
|
+
}
|
|
2668
|
+
this.typingHandlers.clear();
|
|
2215
2669
|
if (this.retryCount < this.options.maxRetries) {
|
|
2216
2670
|
this.state = "reconnecting";
|
|
2217
2671
|
this.notifyStateChange();
|
|
2218
2672
|
this.retryCount++;
|
|
2219
|
-
|
|
2220
|
-
this.
|
|
2673
|
+
const delay = Math.min(
|
|
2674
|
+
this.options.retryInterval * Math.pow(2, this.retryCount - 1),
|
|
2675
|
+
3e4
|
|
2676
|
+
);
|
|
2677
|
+
setTimeout(async () => {
|
|
2678
|
+
try {
|
|
2679
|
+
await this.doConnect();
|
|
2680
|
+
this.log("Reconnected successfully, restoring subscriptions...");
|
|
2681
|
+
await this.restoreSubscriptions(
|
|
2682
|
+
previousSubscriptions,
|
|
2683
|
+
previousPresenceSubscriptions,
|
|
2684
|
+
previousTypingHandlers
|
|
2685
|
+
);
|
|
2686
|
+
} catch (e) {
|
|
2221
2687
|
console.error("[Realtime] Reconnect failed:", e);
|
|
2222
|
-
}
|
|
2223
|
-
},
|
|
2688
|
+
}
|
|
2689
|
+
}, delay);
|
|
2224
2690
|
} else {
|
|
2225
2691
|
this.state = "disconnected";
|
|
2226
2692
|
this.notifyStateChange();
|
|
2227
2693
|
this.notifyError(new Error("Connection lost. Max retries exceeded."));
|
|
2228
2694
|
}
|
|
2229
2695
|
}
|
|
2696
|
+
/**
|
|
2697
|
+
* 이전 구독 상태를 복구합니다.
|
|
2698
|
+
* 재연결 성공 후 호출됩니다.
|
|
2699
|
+
*/
|
|
2700
|
+
async restoreSubscriptions(previousSubscriptions, previousPresenceSubscriptions, previousTypingHandlers) {
|
|
2701
|
+
for (const [category, handlers] of previousSubscriptions) {
|
|
2702
|
+
try {
|
|
2703
|
+
this.log(`Restoring subscription: ${category}`);
|
|
2704
|
+
const requestId = this.generateRequestId();
|
|
2705
|
+
const response = await this.sendRequest({
|
|
2706
|
+
category,
|
|
2707
|
+
action: "subscribe",
|
|
2708
|
+
request_id: requestId
|
|
2709
|
+
});
|
|
2710
|
+
const info = {
|
|
2711
|
+
category: response.category,
|
|
2712
|
+
persist: response.persist,
|
|
2713
|
+
historyCount: response.history_count,
|
|
2714
|
+
readReceipt: response.read_receipt
|
|
2715
|
+
};
|
|
2716
|
+
this.subscriptions.set(category, { info, handlers });
|
|
2717
|
+
this.log(`Restored subscription: ${category}`);
|
|
2718
|
+
} catch (e) {
|
|
2719
|
+
console.error(`[Realtime] Failed to restore subscription for ${category}:`, e);
|
|
2720
|
+
this.notifyError(new Error(`Failed to restore subscription: ${category}`));
|
|
2721
|
+
}
|
|
2722
|
+
}
|
|
2723
|
+
for (const [userId, handlers] of previousPresenceSubscriptions) {
|
|
2724
|
+
try {
|
|
2725
|
+
this.log(`Restoring presence subscription: ${userId}`);
|
|
2726
|
+
const requestId = this.generateRequestId();
|
|
2727
|
+
await this.sendRequest({
|
|
2728
|
+
category: "",
|
|
2729
|
+
action: "presence_subscribe",
|
|
2730
|
+
data: { user_id: userId },
|
|
2731
|
+
request_id: requestId
|
|
2732
|
+
});
|
|
2733
|
+
this.presenceSubscriptions.set(userId, handlers);
|
|
2734
|
+
this.log(`Restored presence subscription: ${userId}`);
|
|
2735
|
+
} catch (e) {
|
|
2736
|
+
console.error(`[Realtime] Failed to restore presence subscription for ${userId}:`, e);
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
for (const [roomId, handlers] of previousTypingHandlers) {
|
|
2740
|
+
try {
|
|
2741
|
+
this.log(`Restoring typing subscription: ${roomId}`);
|
|
2742
|
+
const requestId = this.generateRequestId();
|
|
2743
|
+
await this.sendRequest({
|
|
2744
|
+
category: "",
|
|
2745
|
+
action: "typing_subscribe",
|
|
2746
|
+
data: { room_id: roomId },
|
|
2747
|
+
request_id: requestId
|
|
2748
|
+
});
|
|
2749
|
+
this.typingHandlers.set(roomId, handlers);
|
|
2750
|
+
this.log(`Restored typing subscription: ${roomId}`);
|
|
2751
|
+
} catch (e) {
|
|
2752
|
+
console.error(`[Realtime] Failed to restore typing subscription for ${roomId}:`, e);
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2230
2756
|
sendRequest(message) {
|
|
2231
2757
|
return new Promise((resolve, reject) => {
|
|
2232
2758
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
@@ -2236,7 +2762,7 @@ var RealtimeAPI = class {
|
|
|
2236
2762
|
const timeout = setTimeout(() => {
|
|
2237
2763
|
this.pendingRequests.delete(message.request_id);
|
|
2238
2764
|
reject(new Error("Request timeout"));
|
|
2239
|
-
},
|
|
2765
|
+
}, this.options.timeout);
|
|
2240
2766
|
this.pendingRequests.set(message.request_id, {
|
|
2241
2767
|
resolve,
|
|
2242
2768
|
reject,
|