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/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" || this.state === "connecting") {
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
- return this.doConnect();
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
- this.sendRaw({
1937
- category: "",
1938
- action: "stream",
1939
- data: {
1940
- provider: options.provider,
1941
- model: options.model,
1942
- messages,
1943
- system: options.system,
1944
- temperature: options.temperature,
1945
- max_tokens: options.maxTokens,
1946
- session_id: sessionId,
1947
- metadata: options.metadata
1948
- },
1949
- request_id: requestId
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
- setTimeout(() => {
2207
- this.doConnect().catch((e) => {
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
- }, this.options.retryInterval * this.retryCount);
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
- }, 3e4);
2765
+ }, this.options.timeout);
2227
2766
  this.pendingRequests.set(message.request_id, {
2228
2767
  resolve,
2229
2768
  reject,