connectbase-client 0.6.28 → 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 +229 -7
- package/dist/index.d.ts +229 -7
- package/dist/index.js +564 -25
- package/dist/index.mjs +564 -25
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1573,12 +1573,25 @@ var StorageAPI = class {
|
|
|
1573
1573
|
);
|
|
1574
1574
|
}
|
|
1575
1575
|
/**
|
|
1576
|
-
* 페이지 메타 목록 조회
|
|
1576
|
+
* 페이지 메타 목록 조회 (페이지네이션 지원)
|
|
1577
|
+
*
|
|
1578
|
+
* @example
|
|
1579
|
+
* ```ts
|
|
1580
|
+
* // 기본 조회 (최대 20개)
|
|
1581
|
+
* const { total_count, pages } = await cb.storage.listPageMetas('web-storage-id')
|
|
1582
|
+
*
|
|
1583
|
+
* // 페이지네이션
|
|
1584
|
+
* const page2 = await cb.storage.listPageMetas('web-storage-id', { limit: 10, offset: 10 })
|
|
1585
|
+
* ```
|
|
1577
1586
|
*/
|
|
1578
|
-
async listPageMetas(webStorageId) {
|
|
1587
|
+
async listPageMetas(webStorageId, options) {
|
|
1579
1588
|
const prefix = this.getPublicPrefix();
|
|
1589
|
+
const params = new URLSearchParams();
|
|
1590
|
+
if (options?.limit != null) params.set("limit", String(options.limit));
|
|
1591
|
+
if (options?.offset != null) params.set("offset", String(options.offset));
|
|
1592
|
+
const query = params.toString();
|
|
1580
1593
|
return this.http.get(
|
|
1581
|
-
`${prefix}/storages/webs/${webStorageId}/page-metas`
|
|
1594
|
+
`${prefix}/storages/webs/${webStorageId}/page-metas${query ? `?${query}` : ""}`
|
|
1582
1595
|
);
|
|
1583
1596
|
}
|
|
1584
1597
|
/**
|
|
@@ -1747,6 +1760,15 @@ var RealtimeAPI = class {
|
|
|
1747
1760
|
this.streamSessions = /* @__PURE__ */ new Map();
|
|
1748
1761
|
this.stateHandlers = [];
|
|
1749
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;
|
|
1750
1772
|
this.http = http;
|
|
1751
1773
|
this.socketUrl = socketUrl;
|
|
1752
1774
|
this.clientId = this.generateClientId();
|
|
@@ -1766,14 +1788,22 @@ var RealtimeAPI = class {
|
|
|
1766
1788
|
* - userId: 사용자 식별자 (표시용)
|
|
1767
1789
|
*/
|
|
1768
1790
|
async connect(options = {}) {
|
|
1769
|
-
if (this.state === "connected"
|
|
1791
|
+
if (this.state === "connected") {
|
|
1770
1792
|
return;
|
|
1771
1793
|
}
|
|
1794
|
+
if (this.state === "connecting" && this.connectPromise) {
|
|
1795
|
+
return this.connectPromise;
|
|
1796
|
+
}
|
|
1772
1797
|
this.options = { ...this.options, ...options };
|
|
1773
1798
|
if (options.userId) {
|
|
1774
1799
|
this.userId = options.userId;
|
|
1775
1800
|
}
|
|
1776
|
-
|
|
1801
|
+
this.connectPromise = this.doConnect();
|
|
1802
|
+
try {
|
|
1803
|
+
await this.connectPromise;
|
|
1804
|
+
} finally {
|
|
1805
|
+
this.connectPromise = null;
|
|
1806
|
+
}
|
|
1777
1807
|
}
|
|
1778
1808
|
/**
|
|
1779
1809
|
* 연결 해제
|
|
@@ -1791,6 +1821,20 @@ var RealtimeAPI = class {
|
|
|
1791
1821
|
});
|
|
1792
1822
|
this.pendingRequests.clear();
|
|
1793
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;
|
|
1794
1838
|
}
|
|
1795
1839
|
/**
|
|
1796
1840
|
* 카테고리 구독
|
|
@@ -1826,6 +1870,10 @@ var RealtimeAPI = class {
|
|
|
1826
1870
|
},
|
|
1827
1871
|
onMessage: (handler) => {
|
|
1828
1872
|
handlers.push(handler);
|
|
1873
|
+
return () => {
|
|
1874
|
+
const idx = handlers.indexOf(handler);
|
|
1875
|
+
if (idx > -1) handlers.splice(idx, 1);
|
|
1876
|
+
};
|
|
1829
1877
|
}
|
|
1830
1878
|
};
|
|
1831
1879
|
return subscription;
|
|
@@ -1933,21 +1981,26 @@ var RealtimeAPI = class {
|
|
|
1933
1981
|
handlers,
|
|
1934
1982
|
requestId
|
|
1935
1983
|
});
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
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
|
+
}
|
|
1951
2004
|
return {
|
|
1952
2005
|
sessionId,
|
|
1953
2006
|
stop: async () => {
|
|
@@ -2003,6 +2056,344 @@ var RealtimeAPI = class {
|
|
|
2003
2056
|
if (idx > -1) this.errorHandlers.splice(idx, 1);
|
|
2004
2057
|
};
|
|
2005
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
|
+
}
|
|
2006
2397
|
// Private methods
|
|
2007
2398
|
async doConnect() {
|
|
2008
2399
|
return new Promise((resolve, reject) => {
|
|
@@ -2194,26 +2585,174 @@ var RealtimeAPI = class {
|
|
|
2194
2585
|
}
|
|
2195
2586
|
break;
|
|
2196
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
|
+
}
|
|
2197
2643
|
}
|
|
2198
2644
|
}
|
|
2199
2645
|
handleDisconnect() {
|
|
2200
2646
|
this.ws = null;
|
|
2201
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();
|
|
2202
2669
|
if (this.retryCount < this.options.maxRetries) {
|
|
2203
2670
|
this.state = "reconnecting";
|
|
2204
2671
|
this.notifyStateChange();
|
|
2205
2672
|
this.retryCount++;
|
|
2206
|
-
|
|
2207
|
-
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) {
|
|
2208
2687
|
console.error("[Realtime] Reconnect failed:", e);
|
|
2209
|
-
}
|
|
2210
|
-
},
|
|
2688
|
+
}
|
|
2689
|
+
}, delay);
|
|
2211
2690
|
} else {
|
|
2212
2691
|
this.state = "disconnected";
|
|
2213
2692
|
this.notifyStateChange();
|
|
2214
2693
|
this.notifyError(new Error("Connection lost. Max retries exceeded."));
|
|
2215
2694
|
}
|
|
2216
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
|
+
}
|
|
2217
2756
|
sendRequest(message) {
|
|
2218
2757
|
return new Promise((resolve, reject) => {
|
|
2219
2758
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
@@ -2223,7 +2762,7 @@ var RealtimeAPI = class {
|
|
|
2223
2762
|
const timeout = setTimeout(() => {
|
|
2224
2763
|
this.pendingRequests.delete(message.request_id);
|
|
2225
2764
|
reject(new Error("Request timeout"));
|
|
2226
|
-
},
|
|
2765
|
+
}, this.options.timeout);
|
|
2227
2766
|
this.pendingRequests.set(message.request_id, {
|
|
2228
2767
|
resolve,
|
|
2229
2768
|
reject,
|