@vibexnpm/talkx 3.0.6 → 3.0.7
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.d.ts +56 -2
- package/dist/talkflow-sdk.esm.js +1 -1
- package/dist/talkflow-sdk.esm.js.map +1 -1
- package/dist/talkflow-sdk.standalone.js +1 -1
- package/dist/talkflow-sdk.standalone.js.map +1 -1
- package/dist/talkflow-sdk.umd.js +1 -1
- package/dist/talkflow-sdk.umd.js.map +1 -1
- package/package.json +3 -1
- package/src/TalkFlowClient.js +13 -0
- package/src/chat/ChatClient.js +371 -5
- package/src/constants.js +13 -0
- package/src/core/ConnectionManager.js +214 -1
- package/src/talkflow/eventForwarding.js +8 -0
- package/src/talkflow/session.js +12 -0
- package/types/index.d.ts +56 -2
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import EventEmitter from '../utils/EventEmitter.js';
|
|
7
7
|
import Logger from '../utils/Logger.js';
|
|
8
8
|
import { ConnectionState, ErrorTypes, WebSocketPaths, DefaultConfig, LogLevel } from '../constants.js';
|
|
9
|
-
import { Client as StompClient } from '@stomp/stompjs';
|
|
9
|
+
import { Client as StompClient, ReconnectionTimeMode, StompSocketState } from '@stomp/stompjs';
|
|
10
10
|
import SockJS from 'sockjs-client';
|
|
11
11
|
|
|
12
12
|
// STOMP Client 가져오기 (CDN 사용 시 전역 객체 우선)
|
|
@@ -51,9 +51,11 @@ class ConnectionManager extends EventEmitter {
|
|
|
51
51
|
this.useSockJS = options.useSockJS !== false;
|
|
52
52
|
|
|
53
53
|
this.reconnectDelay = options.reconnectDelay || DefaultConfig.reconnectDelay;
|
|
54
|
+
this.maxReconnectDelay = options.maxReconnectDelay || DefaultConfig.maxReconnectDelay;
|
|
54
55
|
this.maxReconnectAttempts = options.maxReconnectAttempts || DefaultConfig.maxReconnectAttempts;
|
|
55
56
|
this.heartbeatIncoming = options.heartbeatIncoming || DefaultConfig.heartbeatIncoming;
|
|
56
57
|
this.heartbeatOutgoing = options.heartbeatOutgoing || DefaultConfig.heartbeatOutgoing;
|
|
58
|
+
this.resumeOnVisibility = options.resumeOnVisibility !== false;
|
|
57
59
|
|
|
58
60
|
this.logger = new Logger(options.logLevel || LogLevel.WARN, 'ConnectionManager');
|
|
59
61
|
|
|
@@ -64,6 +66,191 @@ class ConnectionManager extends EventEmitter {
|
|
|
64
66
|
this.subscriptions = new Map();
|
|
65
67
|
this.pendingSubscriptions = [];
|
|
66
68
|
this.userId = null;
|
|
69
|
+
|
|
70
|
+
// 사용자가 명시적으로 끊은 상태인지 — 포그라운드 복귀가 이걸 되살리면 안 된다.
|
|
71
|
+
this._intentionalDisconnect = false;
|
|
72
|
+
// forceReconnect 의 teardown 중 발생하는 close 이벤트를 재연결 시도로 오해하지 않도록.
|
|
73
|
+
this._tearingDown = false;
|
|
74
|
+
// connect() 가 최소 1회 호출됐는지 — 복귀 자동 재연결의 전제 조건.
|
|
75
|
+
this._connectRequested = false;
|
|
76
|
+
// forceReconnect 단일 실행 보장 (복귀 이벤트 3종이 거의 동시에 들어온다).
|
|
77
|
+
this._forceReconnectPromise = null;
|
|
78
|
+
this._lifecycleHandlers = null;
|
|
79
|
+
|
|
80
|
+
this._installLifecycleListeners();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 포그라운드 복귀 / 네트워크 복구 감지 (RECONNECT_SYNC_DESIGN §6.1).
|
|
85
|
+
*
|
|
86
|
+
* <p>이게 없으면 백그라운드에서 재시도 횟수를 소진해 ERROR 로 죽은 연결이 복귀해도 되살아나지 않아
|
|
87
|
+
* <b>새로고침만이 유일한 복구 수단</b>이 된다. 모바일은 OS 가 타이머까지 얼려서 재시도 루프 자체가
|
|
88
|
+
* 돌지 못하므로 복귀 시점의 능동적 깨우기가 필수다.</p>
|
|
89
|
+
*
|
|
90
|
+
* <p>SSR/Node 환경에서는 no-op.</p>
|
|
91
|
+
*
|
|
92
|
+
* @private
|
|
93
|
+
*/
|
|
94
|
+
_installLifecycleListeners() {
|
|
95
|
+
if (typeof window === 'undefined' || typeof window.addEventListener !== 'function') {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const onVisibilityChange = () => {
|
|
100
|
+
if (typeof document !== 'undefined' && document.visibilityState === 'visible') {
|
|
101
|
+
this._onResume('visibilitychange');
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
// bfcache 복원 — visibilitychange 가 안 오는 경로가 있어 별도로 받는다.
|
|
105
|
+
const onPageShow = () => this._onResume('pageshow');
|
|
106
|
+
const onOnline = () => this._onResume('online');
|
|
107
|
+
|
|
108
|
+
if (typeof document !== 'undefined' && typeof document.addEventListener === 'function') {
|
|
109
|
+
document.addEventListener('visibilitychange', onVisibilityChange);
|
|
110
|
+
}
|
|
111
|
+
window.addEventListener('pageshow', onPageShow);
|
|
112
|
+
window.addEventListener('online', onOnline);
|
|
113
|
+
|
|
114
|
+
this._lifecycleHandlers = { onVisibilityChange, onPageShow, onOnline };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** @private */
|
|
118
|
+
_removeLifecycleListeners() {
|
|
119
|
+
if (!this._lifecycleHandlers) return;
|
|
120
|
+
|
|
121
|
+
const { onVisibilityChange, onPageShow, onOnline } = this._lifecycleHandlers;
|
|
122
|
+
if (typeof document !== 'undefined' && typeof document.removeEventListener === 'function') {
|
|
123
|
+
document.removeEventListener('visibilitychange', onVisibilityChange);
|
|
124
|
+
}
|
|
125
|
+
if (typeof window !== 'undefined' && typeof window.removeEventListener === 'function') {
|
|
126
|
+
window.removeEventListener('pageshow', onPageShow);
|
|
127
|
+
window.removeEventListener('online', onOnline);
|
|
128
|
+
}
|
|
129
|
+
this._lifecycleHandlers = null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* 복귀 시점 판정.
|
|
134
|
+
*
|
|
135
|
+
* <p><b>이벤트 계약</b> — {@code resumed} 와 {@code reconnected} 는 <b>상호 배타적</b>이며 둘을 합쳐야
|
|
136
|
+
* "재동기화가 필요한 시점" 전체를 덮는다:</p>
|
|
137
|
+
* <ul>
|
|
138
|
+
* <li>{@code resumed} — 복귀했는데 연결이 <b>살아 있어 재연결이 필요 없었다</b>. 그래도 백그라운드
|
|
139
|
+
* 동안 프레임을 놓쳤을 수 있으므로 갭 복구는 돌아야 한다.</li>
|
|
140
|
+
* <li>{@code reconnected} — 재연결에 <b>성공</b>했다. 복귀가 유발한 경우든(좀비 소켓/ERROR 복구)
|
|
141
|
+
* 탭이 열린 채 일시 끊겼다 붙은 경우든 이 이벤트로 수렴한다.</li>
|
|
142
|
+
* </ul>
|
|
143
|
+
*
|
|
144
|
+
* <p>재연결이 일어나는 경로에서는 {@code resumed} 를 발행하지 않는다 — 하나의 복귀에 두 이벤트가
|
|
145
|
+
* 겹쳐 나가면 소비자가 갭 복구를 두 번 돌린다. 소비자는 <b>둘 다</b> 구독해야 한다.</p>
|
|
146
|
+
*
|
|
147
|
+
* @private
|
|
148
|
+
*/
|
|
149
|
+
_onResume(reason) {
|
|
150
|
+
if (!this.resumeOnVisibility || this._intentionalDisconnect) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// connect() 를 아직 한 번도 호출하지 않았으면 복귀가 연결을 만들어내면 안 된다.
|
|
155
|
+
// 이 경로는 TalkFlowClient.connect() 를 우회하므로 roomList 자동 구독 / WebRTC enable 등
|
|
156
|
+
// 상위 초기화가 통째로 빠진 "반쪽 연결" 이 된다. 최초 연결은 반드시 사용자 호출로 시작한다.
|
|
157
|
+
// (1회 연결된 뒤의 재연결은 _restoreSubscriptions 가 STOMP 구독을 되살리므로 안전하다.)
|
|
158
|
+
if (!this._connectRequested) {
|
|
159
|
+
this.logger.debug(`Resume (${reason}) ignored — connect() has not been called yet`);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// 연결 시도 중이거나 강제 재연결이 진행 중이면 그대로 둔다 — 중복 activate 방지.
|
|
164
|
+
if (this.state === ConnectionState.CONNECTING || this._forceReconnectPromise) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (this.state === ConnectionState.CONNECTED) {
|
|
169
|
+
if (this._isSocketAlive()) {
|
|
170
|
+
this.logger.debug(`Resume (${reason}): connection alive`);
|
|
171
|
+
this.emit('resumed', { reason });
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
// 좀비 소켓 — STOMP 가 아직 close 를 인지하지 못한 상태.
|
|
175
|
+
this.logger.warn(`Resume (${reason}): zombie socket detected, forcing reconnect`);
|
|
176
|
+
this.forceReconnect(`zombie-socket:${reason}`);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// DISCONNECTED(비의도) / RECONNECTING / ERROR — 재시도 횟수를 리셋하고 즉시 되살린다.
|
|
181
|
+
this.logger.info(`Resume (${reason}): reviving connection from state=${this.state}`);
|
|
182
|
+
this.forceReconnect(reason);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* 소켓이 실제로 열려 있는지.
|
|
187
|
+
*
|
|
188
|
+
* <p>{@code state === CONNECTED} 만으로는 부족하다 — OS 가 백그라운드에서 소켓을 끊어도
|
|
189
|
+
* STOMP 가 close 를 인지하기 전까지 상태는 CONNECTED 로 남는다. 반대로 readyState 가 OPEN 인데
|
|
190
|
+
* 실제로는 죽은 경우는 STOMP 자체의 incoming heartbeat 감시가 닫아주고, 그 사이 유실분은
|
|
191
|
+
* 갭 복구가 메운다.</p>
|
|
192
|
+
*
|
|
193
|
+
* @private
|
|
194
|
+
*/
|
|
195
|
+
_isSocketAlive() {
|
|
196
|
+
const socket = this.stompClient && this.stompClient.webSocket;
|
|
197
|
+
if (!socket) return false;
|
|
198
|
+
return socket.readyState === StompSocketState.OPEN;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* 강제 재연결 — 기존 클라이언트를 완전히 정리한 뒤 새로 활성화.
|
|
203
|
+
*
|
|
204
|
+
* <p>ERROR 상태({@link #_handleReconnect} 가 {@code deactivate} 로 STOMP 루프를 끈 상태)에서도
|
|
205
|
+
* 되살아나야 하므로 {@code activate} 재호출이 필요하다.</p>
|
|
206
|
+
*
|
|
207
|
+
* @param {string} reason
|
|
208
|
+
* @returns {Promise<void>}
|
|
209
|
+
*/
|
|
210
|
+
async forceReconnect(reason) {
|
|
211
|
+
if (this._intentionalDisconnect || this.state === ConnectionState.CONNECTING) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// 단일 실행 — 복귀 시 visibilitychange / pageshow / online 이 거의 동시에 들어오는데,
|
|
216
|
+
// deactivate() 를 await 하는 동안 state 는 아직 CONNECTED 라 재진입 가드가 없으면
|
|
217
|
+
// 두 번째 resume 이 그대로 통과해 클라이언트를 이중으로 만든다.
|
|
218
|
+
if (this._forceReconnectPromise) {
|
|
219
|
+
this.logger.debug(`Force reconnect already in progress, joining: ${reason}`);
|
|
220
|
+
return this._forceReconnectPromise;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
this._forceReconnectPromise = this._doForceReconnect(reason)
|
|
224
|
+
.finally(() => { this._forceReconnectPromise = null; });
|
|
225
|
+
|
|
226
|
+
return this._forceReconnectPromise;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** @private */
|
|
230
|
+
async _doForceReconnect(reason) {
|
|
231
|
+
this.logger.info(`Force reconnect: ${reason}`);
|
|
232
|
+
|
|
233
|
+
if (this.stompClient) {
|
|
234
|
+
this._tearingDown = true;
|
|
235
|
+
try {
|
|
236
|
+
await this.stompClient.deactivate();
|
|
237
|
+
} catch (error) {
|
|
238
|
+
this.logger.warn('Error deactivating stomp client on force reconnect:', error);
|
|
239
|
+
} finally {
|
|
240
|
+
this._tearingDown = false;
|
|
241
|
+
}
|
|
242
|
+
this.stompClient = null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// connect() 가 CONNECTED/CONNECTING 이면 조기 반환하므로 상태를 먼저 내려둔다.
|
|
246
|
+
this._setState(ConnectionState.DISCONNECTED);
|
|
247
|
+
this.reconnectAttempts = 0;
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
await this.connect();
|
|
251
|
+
} catch (error) {
|
|
252
|
+
this.logger.warn(`Force reconnect failed (${reason}):`, error);
|
|
253
|
+
}
|
|
67
254
|
}
|
|
68
255
|
|
|
69
256
|
/**
|
|
@@ -112,6 +299,10 @@ class ConnectionManager extends EventEmitter {
|
|
|
112
299
|
heartbeatIncoming: this.heartbeatIncoming,
|
|
113
300
|
heartbeatOutgoing: this.heartbeatOutgoing,
|
|
114
301
|
reconnectDelay: this.reconnectDelay,
|
|
302
|
+
// 지수 백오프 — 고정 간격이면 서버 재시작 시 전 클라이언트가 같은 주기로 몰린다.
|
|
303
|
+
// STOMP 7 내장 기능이라 직접 타이머를 굴리지 않는다.
|
|
304
|
+
reconnectTimeMode: ReconnectionTimeMode.EXPONENTIAL,
|
|
305
|
+
maxReconnectDelay: this.maxReconnectDelay,
|
|
115
306
|
debug: (str) => {
|
|
116
307
|
if (this.logger.level <= LogLevel.DEBUG) {
|
|
117
308
|
this.logger.debug('STOMP:', str);
|
|
@@ -155,6 +346,8 @@ class ConnectionManager extends EventEmitter {
|
|
|
155
346
|
|
|
156
347
|
this._setState(ConnectionState.CONNECTING);
|
|
157
348
|
this.reconnectAttempts = 0;
|
|
349
|
+
this._intentionalDisconnect = false;
|
|
350
|
+
this._connectRequested = true;
|
|
158
351
|
|
|
159
352
|
return new Promise((resolve, reject) => {
|
|
160
353
|
this._connectResolve = resolve;
|
|
@@ -176,8 +369,13 @@ class ConnectionManager extends EventEmitter {
|
|
|
176
369
|
* @private
|
|
177
370
|
*/
|
|
178
371
|
_onConnect(frame) {
|
|
372
|
+
// 재연결 여부 판정은 카운터 리셋 전에 — 갭 복구(S4)가 이 신호로 동기화를 건다.
|
|
373
|
+
const attempts = this.reconnectAttempts;
|
|
374
|
+
const isReconnect = attempts > 0 || this._hasConnectedBefore === true;
|
|
375
|
+
|
|
179
376
|
this._setState(ConnectionState.CONNECTED);
|
|
180
377
|
this.reconnectAttempts = 0;
|
|
378
|
+
this._hasConnectedBefore = true;
|
|
181
379
|
this.logger.info('Connected to server', frame);
|
|
182
380
|
|
|
183
381
|
// 기존 구독 재등록 (재연결 시)
|
|
@@ -188,6 +386,11 @@ class ConnectionManager extends EventEmitter {
|
|
|
188
386
|
|
|
189
387
|
this.emit('connected', { frame });
|
|
190
388
|
|
|
389
|
+
// 재연결 '성공' 신호 — 기존에는 성공 이벤트가 없어 connected 재발행으로만 유추해야 했다.
|
|
390
|
+
if (isReconnect) {
|
|
391
|
+
this.emit('reconnected', { attempts });
|
|
392
|
+
}
|
|
393
|
+
|
|
191
394
|
if (this._connectResolve) {
|
|
192
395
|
this._connectResolve();
|
|
193
396
|
this._connectResolve = null;
|
|
@@ -238,6 +441,13 @@ class ConnectionManager extends EventEmitter {
|
|
|
238
441
|
_onWebSocketClose(event) {
|
|
239
442
|
this.logger.warn('WebSocket closed:', event);
|
|
240
443
|
|
|
444
|
+
// forceReconnect 의 의도적 teardown 이면 재연결 시도로 집계하지 않는다 —
|
|
445
|
+
// 카운터가 오염되면 복귀 직후 다시 max 를 소진해버린다.
|
|
446
|
+
if (this._tearingDown) {
|
|
447
|
+
this.logger.debug('WebSocket closed during intentional teardown — not counting as reconnect');
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
|
|
241
451
|
if (this.state !== ConnectionState.DISCONNECTED) {
|
|
242
452
|
this._handleReconnect();
|
|
243
453
|
}
|
|
@@ -445,6 +655,8 @@ class ConnectionManager extends EventEmitter {
|
|
|
445
655
|
* clear 만 하면 호출자 {@code await} 가 영원히 hang 된다.</p>
|
|
446
656
|
*/
|
|
447
657
|
async disconnect() {
|
|
658
|
+
// 명시적 해제 — 포그라운드 복귀가 이걸 되살리면 안 된다.
|
|
659
|
+
this._intentionalDisconnect = true;
|
|
448
660
|
this._drainPendingSubscriptions('Disconnected before subscription completed');
|
|
449
661
|
|
|
450
662
|
if (this.stompClient) {
|
|
@@ -508,6 +720,7 @@ class ConnectionManager extends EventEmitter {
|
|
|
508
720
|
* ConnectionManager 를 직접 소유한 사용자가 destroy 만 호출하는 경로도 안전해야 함.</p>
|
|
509
721
|
*/
|
|
510
722
|
async destroy() {
|
|
723
|
+
this._removeLifecycleListeners();
|
|
511
724
|
await this.disconnect();
|
|
512
725
|
this.removeAllListeners();
|
|
513
726
|
this.logger.info('ConnectionManager destroyed');
|
|
@@ -2,10 +2,18 @@ const CONNECTION_EVENT_MAP = [
|
|
|
2
2
|
['connected', 'connected'],
|
|
3
3
|
['disconnected', 'disconnected'],
|
|
4
4
|
['reconnecting', 'reconnecting'],
|
|
5
|
+
// 재연결 '성공' + 포그라운드 복귀 감지 (RECONNECT_SYNC_DESIGN §6.1/§6.4)
|
|
6
|
+
['reconnected', 'reconnected'],
|
|
7
|
+
['resumed', 'resumed'],
|
|
5
8
|
['error', 'connectionError']
|
|
6
9
|
];
|
|
7
10
|
|
|
8
11
|
const CHAT_EVENT_MAP = [
|
|
12
|
+
// 갭 복구 (RECONNECT_SYNC_DESIGN §6.4) — 기본 메시지 수신 코드는 변경 불필요, 선택 구독용.
|
|
13
|
+
['syncStarted', 'syncStarted'],
|
|
14
|
+
['syncCompleted', 'syncCompleted'],
|
|
15
|
+
['gapTruncated', 'gapTruncated'],
|
|
16
|
+
['roomListSynced', 'roomListSynced'],
|
|
9
17
|
['message', 'chatMessage'],
|
|
10
18
|
['newMessage', 'newChatMessage'],
|
|
11
19
|
['messageUpdated', 'messageUpdated'],
|
package/src/talkflow/session.js
CHANGED
|
@@ -20,7 +20,11 @@ export function initializeSubClients(client) {
|
|
|
20
20
|
projectId: client.options.projectId,
|
|
21
21
|
useSockJS: client.options.useSockJS,
|
|
22
22
|
reconnectDelay: client.options.reconnectDelay,
|
|
23
|
+
maxReconnectDelay: client.options.maxReconnectDelay,
|
|
23
24
|
maxReconnectAttempts: client.options.maxReconnectAttempts,
|
|
25
|
+
heartbeatIncoming: client.options.heartbeatIncoming,
|
|
26
|
+
heartbeatOutgoing: client.options.heartbeatOutgoing,
|
|
27
|
+
resumeOnVisibility: client.options.resumeOnVisibility,
|
|
24
28
|
logLevel: client.options.logLevel
|
|
25
29
|
});
|
|
26
30
|
|
|
@@ -28,6 +32,10 @@ export function initializeSubClients(client) {
|
|
|
28
32
|
connectionManager: client.connectionManager,
|
|
29
33
|
apiClient: client.apiClient,
|
|
30
34
|
userId: client._userId,
|
|
35
|
+
syncOnReconnect: client.options.syncOnReconnect,
|
|
36
|
+
rewindMillis: client.options.rewindMillis,
|
|
37
|
+
maxSyncPages: client.options.maxSyncPages,
|
|
38
|
+
syncPageSize: client.options.syncPageSize,
|
|
31
39
|
logLevel: client.options.logLevel
|
|
32
40
|
});
|
|
33
41
|
|
|
@@ -330,6 +338,10 @@ export function applySessionMethods(TalkFlowClient) {
|
|
|
330
338
|
|
|
331
339
|
if (needReinitialize && this.connectionManager) {
|
|
332
340
|
await this.disconnect();
|
|
341
|
+
// destroy() 로 버려야 lifecycle listener(visibilitychange/pageshow/online)가 해제된다.
|
|
342
|
+
// disconnect() 만 하고 참조를 끊으면 죽은 매니저가 window 에 매달린 채 남아,
|
|
343
|
+
// 사용자 전환이 잦은 앱에서 복귀 이벤트마다 유령 매니저가 함께 깨어난다.
|
|
344
|
+
await this.connectionManager.destroy();
|
|
333
345
|
if (this.pushManager) {
|
|
334
346
|
this.pushManager.reset();
|
|
335
347
|
this._pushEnablePromise = null;
|
package/types/index.d.ts
CHANGED
|
@@ -926,10 +926,35 @@ export interface TalkFlowClientOptions {
|
|
|
926
926
|
serverUrl?: string;
|
|
927
927
|
/** SockJS 사용 여부. 기본 true. */
|
|
928
928
|
useSockJS?: boolean;
|
|
929
|
-
/** 재연결 지연 (ms). 기본 5000. */
|
|
929
|
+
/** 최초 재연결 지연 (ms). 기본 5000. 이후 지수적으로 증가한다. */
|
|
930
930
|
reconnectDelay?: number;
|
|
931
|
+
/** 지수 백오프 상한 (ms). 기본 30000. */
|
|
932
|
+
maxReconnectDelay?: number;
|
|
931
933
|
/** 최대 재연결 시도. 기본 10. */
|
|
932
934
|
maxReconnectAttempts?: number;
|
|
935
|
+
/** 수신 하트비트 간격 (ms). 기본 10000. */
|
|
936
|
+
heartbeatIncoming?: number;
|
|
937
|
+
/** 발신 하트비트 간격 (ms). 기본 10000. */
|
|
938
|
+
heartbeatOutgoing?: number;
|
|
939
|
+
/**
|
|
940
|
+
* 탭 복귀(visibilitychange/pageshow) · 네트워크 복구(online) 시 자동 재연결. 기본 true.
|
|
941
|
+
*
|
|
942
|
+
* <p>재시도 횟수를 소진해 죽은 연결도 되살린다. 단 {@code connect()} 를 한 번도 호출하지 않았거나
|
|
943
|
+
* {@code disconnect()} 로 명시적으로 끊은 경우에는 동작하지 않는다.</p>
|
|
944
|
+
*/
|
|
945
|
+
resumeOnVisibility?: boolean;
|
|
946
|
+
/**
|
|
947
|
+
* 재연결/복귀 후 끊긴 구간의 메시지를 자동으로 다시 불러올지. 기본 true.
|
|
948
|
+
*
|
|
949
|
+
* <p>복구된 메시지는 일반 `chatMessage` 이벤트로 재생되므로 수신 코드를 바꿀 필요가 없다.</p>
|
|
950
|
+
*/
|
|
951
|
+
syncOnReconnect?: boolean;
|
|
952
|
+
/** 갭 복구 첫 페이지에만 적용되는 되감기 폭 (ms). 기본 3000. 서버가 상한 clamp 후 1회 차감. */
|
|
953
|
+
rewindMillis?: number;
|
|
954
|
+
/** 갭 복구 페이지 상한. 기본 5. 초과 시 최신 페이지 재로드로 전환하고 `gapTruncated` 를 발행. */
|
|
955
|
+
maxSyncPages?: number;
|
|
956
|
+
/** 갭 복구 페이지 크기. 기본 100. */
|
|
957
|
+
syncPageSize?: number;
|
|
933
958
|
/** WebRTC ICE 서버. */
|
|
934
959
|
iceServers?: RTCIceServer[];
|
|
935
960
|
/** connect() 시 채팅방 리스트 자동 구독. 기본 true. */
|
|
@@ -1229,7 +1254,7 @@ export interface RetentionCleanupPayload {
|
|
|
1229
1254
|
/**
|
|
1230
1255
|
* TalkFlowClient 이벤트 맵 — 런타임 {@code TalkFlowClient.js} 의 {@code this.emit(...)} 호출 기반.
|
|
1231
1256
|
*
|
|
1232
|
-
* <p><b>Connection 관련</b>: stateChange, connected, disconnected, reconnecting, connectionError, tokenSet, loggedOut</p>
|
|
1257
|
+
* <p><b>Connection 관련</b>: stateChange, connected, disconnected, reconnecting, reconnected, resumed, connectionError, tokenSet, loggedOut</p>
|
|
1233
1258
|
* <p><b>Chat 관련 (ChatClient 이벤트를 이 이름으로 re-emit)</b>: chatMessage, newChatMessage, messageUpdated, messageDeleted, reactionChanged, linkPreviewAttached, messageRead, typing, assistantProgress, memberJoined, memberLeft, roomSubscribed, roomUnsubscribed, roomListSubscribed, roomListUnsubscribed, roomListUpdate, roomListMessage, roomListCreated, roomListJoined, roomListLeft, roomListSelfLeft, roomListRoomUpdated, retentionCleanup</p>
|
|
1234
1259
|
* <p><b>Push 관련</b>: pushEnabled, pushFailed, pushNotification</p>
|
|
1235
1260
|
* <p><b>WebRTC 관련</b>: localStreamStarted, localStreamStopped, remoteTrack, screenShareStarted, screenShareEnded, deviceChange, mediaStateChanged, callStarted, callEnded, callRequested, callAccepted, callRejected, callCancelled, callInvitation, callBusy, incomingCall, incomingCallWhileBusy, userJoined, userLeft, participantLeft, participantMediaState, peerConnected, peerDisconnected, peerClosed, webrtcError</p>
|
|
@@ -1240,6 +1265,20 @@ export interface TalkFlowClientEvents {
|
|
|
1240
1265
|
connected: unknown;
|
|
1241
1266
|
disconnected: unknown;
|
|
1242
1267
|
reconnecting: { attempt: number };
|
|
1268
|
+
/**
|
|
1269
|
+
* 재연결 **성공**. 최초 연결에는 발행되지 않는다.
|
|
1270
|
+
*
|
|
1271
|
+
* <p>{@link TalkFlowClientEvents.resumed} 와 상호 배타적 — 재동기화 시점을 모두 잡으려면 둘 다 구독한다.</p>
|
|
1272
|
+
*/
|
|
1273
|
+
reconnected: { attempts: number };
|
|
1274
|
+
/**
|
|
1275
|
+
* 탭 복귀 / 네트워크 복구 감지 — **연결이 살아 있어 재연결이 불필요했던** 경우에만 발행된다.
|
|
1276
|
+
*
|
|
1277
|
+
* <p>백그라운드 동안 프레임을 놓쳤을 수 있으므로 소비자는 이 시점에도 재동기화해야 한다.
|
|
1278
|
+
* 복귀가 재연결을 유발한 경우에는 이 이벤트 대신 {@link TalkFlowClientEvents.reconnected} 가
|
|
1279
|
+
* 발행된다(두 이벤트가 겹치지 않아 재동기화가 중복 실행되지 않는다).</p>
|
|
1280
|
+
*/
|
|
1281
|
+
resumed: { reason: 'visibilitychange' | 'pageshow' | 'online' };
|
|
1243
1282
|
connectionError: unknown;
|
|
1244
1283
|
tokenSet: { userId: string };
|
|
1245
1284
|
loggedOut: void;
|
|
@@ -1288,6 +1327,21 @@ export interface TalkFlowClientEvents {
|
|
|
1288
1327
|
roomListRoomUpdated: RoomListUpdateEvent;
|
|
1289
1328
|
retentionCleanup: RetentionCleanupPayload;
|
|
1290
1329
|
|
|
1330
|
+
// 갭 복구 — 복구된 메시지 자체는 일반 chatMessage 로 재생되므로 아래는 모두 선택 구독용.
|
|
1331
|
+
/** 갭 복구 시작 (전 방 1회). */
|
|
1332
|
+
syncStarted: { roomIds: string[]; reason: string };
|
|
1333
|
+
/** 갭 복구 완료 (전 방 1회). */
|
|
1334
|
+
syncCompleted: {
|
|
1335
|
+
rooms: Array<{ roomId: string; recovered: number; truncated: boolean; durationMs: number }>;
|
|
1336
|
+
totalRecovered: number;
|
|
1337
|
+
durationMs: number;
|
|
1338
|
+
reason: string;
|
|
1339
|
+
};
|
|
1340
|
+
/** 갭이 `maxSyncPages` 를 넘거나 커서가 만료돼 최신 페이지 재로드로 전환됨 (방 단위). */
|
|
1341
|
+
gapTruncated: { roomId: string; fetched: number; reason?: 'cursorExpired' };
|
|
1342
|
+
/** 재연결 시 방 목록 전체 스냅샷. delta 계약인 `roomListUpdate` 와 별개. */
|
|
1343
|
+
roomListSynced: { rooms: ChatRoomResponse[]; reason: string };
|
|
1344
|
+
|
|
1291
1345
|
// Push
|
|
1292
1346
|
pushEnabled: void;
|
|
1293
1347
|
/**
|