@vibexnpm/talkx 3.0.6 → 3.0.8
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 +78 -16
- 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 +78 -16
|
@@ -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
|
@@ -332,16 +332,16 @@ export interface RoomSummaryRollup {
|
|
|
332
332
|
format: string;
|
|
333
333
|
/** 이 rollup 의 언어(소문자 BCP-47 / {@code 'OFF'}=원문). {@link RoomSummaryView.language} 와 동일. */
|
|
334
334
|
language: string;
|
|
335
|
-
/** rollup 이 커버하는 가장 오래된
|
|
336
|
-
coverStartAt:
|
|
337
|
-
/** 마지막으로 요약된
|
|
338
|
-
watermarkAt:
|
|
335
|
+
/** rollup 이 커버하는 가장 오래된 지점(epoch millis). */
|
|
336
|
+
coverStartAt: number | null;
|
|
337
|
+
/** 마지막으로 요약된 지점(epoch millis). */
|
|
338
|
+
watermarkAt: number | null;
|
|
339
339
|
/** 전체 segment 수(페이지 총량 힌트). */
|
|
340
340
|
segmentCount: number;
|
|
341
341
|
/** 이 키(포맷×언어)가 최신 대화를 아직 반영 안 함(reducedSegmentCount < segmentCount) — refreshSummary 로 최신화 가능. */
|
|
342
342
|
stale: boolean;
|
|
343
|
-
/** 마지막 갱신
|
|
344
|
-
updatedAt:
|
|
343
|
+
/** 마지막 갱신 시각(epoch millis). */
|
|
344
|
+
updatedAt: number;
|
|
345
345
|
}
|
|
346
346
|
|
|
347
347
|
/** Rolling 누적 요약 구간 부분요약 — {@link RoomSummaryView.segments} 항목. 항상 {@code contentFormat==='neutral'}. */
|
|
@@ -352,9 +352,9 @@ export interface RoomSummarySegment {
|
|
|
352
352
|
content: string;
|
|
353
353
|
/** 항상 {@code 'neutral'}(format 미적용). */
|
|
354
354
|
contentFormat: string;
|
|
355
|
-
fromSentAt:
|
|
355
|
+
fromSentAt: number | null;
|
|
356
356
|
fromMessageId: string | null;
|
|
357
|
-
toSentAt:
|
|
357
|
+
toSentAt: number | null;
|
|
358
358
|
toMessageId: string | null;
|
|
359
359
|
/** 이 구간이 담은 메시지 수. */
|
|
360
360
|
messageCount: number;
|
|
@@ -407,8 +407,8 @@ export interface SummaryUpdateEvent {
|
|
|
407
407
|
* 그 외 null. 각 사용자는 자기 언어가 이 목록에 있으면 {@code getSummary({ format, lang })} 로 읽는다(§S1).
|
|
408
408
|
*/
|
|
409
409
|
languages: string[] | null;
|
|
410
|
-
/** 갱신
|
|
411
|
-
updatedAt:
|
|
410
|
+
/** 갱신 시각(epoch millis). {@code READY} 만 포함, 그 외 null. */
|
|
411
|
+
updatedAt: number | null;
|
|
412
412
|
}
|
|
413
413
|
|
|
414
414
|
/**
|
|
@@ -723,7 +723,8 @@ export interface ParticipantResponse {
|
|
|
723
723
|
* 과도기 동안 {@code ADMIN} 생성자가 보일 수 있으나 권한은 동일.</p>
|
|
724
724
|
*/
|
|
725
725
|
role: 'OWNER' | 'ADMIN' | 'MEMBER' | 'ASSISTANT';
|
|
726
|
-
|
|
726
|
+
/** 참가 시각(epoch millis). */
|
|
727
|
+
joinedAt: number;
|
|
727
728
|
}
|
|
728
729
|
|
|
729
730
|
/** 채팅방 last message preview. */
|
|
@@ -734,7 +735,8 @@ export interface LastMessageResponse {
|
|
|
734
735
|
senderProfileUrl?: string | null;
|
|
735
736
|
content: string;
|
|
736
737
|
messageType: string;
|
|
737
|
-
|
|
738
|
+
/** 마지막 메시지 시각(epoch millis). */
|
|
739
|
+
sentAt: number;
|
|
738
740
|
/**
|
|
739
741
|
* 파일 메시지일 때 첫 파일의 사용자 metadata (opaque pass-through).
|
|
740
742
|
* <p>고객사가 파일 업로드 시 함께 보낸 데이터 (스티커 ID 등) 가 채팅방 리스트에서도 식별 가능하도록 보존.
|
|
@@ -771,8 +773,10 @@ export interface ChatRoomResponse {
|
|
|
771
773
|
pinnedMessageId?: string | null;
|
|
772
774
|
participants?: ParticipantResponse[];
|
|
773
775
|
lastMessage?: LastMessageResponse | null;
|
|
774
|
-
|
|
775
|
-
|
|
776
|
+
/** 마지막 메시지 시각(epoch millis) — 방 목록 정렬용. */
|
|
777
|
+
lastMessageAt?: number | null;
|
|
778
|
+
/** 방 생성 시각(epoch millis). */
|
|
779
|
+
createdAt: number;
|
|
776
780
|
}
|
|
777
781
|
|
|
778
782
|
/** 커서 페이지네이션 응답. */
|
|
@@ -825,6 +829,10 @@ export interface RoomListUpdateEvent {
|
|
|
825
829
|
eventType: RoomListEventTypeValue;
|
|
826
830
|
roomId: string;
|
|
827
831
|
actorId?: string;
|
|
832
|
+
/** {@code MESSAGE_*} 이벤트의 방 마지막 메시지 시각(epoch millis). 그 외 이벤트에서는 undefined. */
|
|
833
|
+
lastMessageAt?: number | null;
|
|
834
|
+
/** 이벤트 발행 시각(epoch millis). */
|
|
835
|
+
timestamp?: number;
|
|
828
836
|
cutoffTime?: number;
|
|
829
837
|
/** {@code ROOM_UPDATED} 전용 — 저장된 최신 메타 snapshot. 다른 eventType 에서는 undefined. */
|
|
830
838
|
room?: RoomMeta;
|
|
@@ -926,10 +934,35 @@ export interface TalkFlowClientOptions {
|
|
|
926
934
|
serverUrl?: string;
|
|
927
935
|
/** SockJS 사용 여부. 기본 true. */
|
|
928
936
|
useSockJS?: boolean;
|
|
929
|
-
/** 재연결 지연 (ms). 기본 5000. */
|
|
937
|
+
/** 최초 재연결 지연 (ms). 기본 5000. 이후 지수적으로 증가한다. */
|
|
930
938
|
reconnectDelay?: number;
|
|
939
|
+
/** 지수 백오프 상한 (ms). 기본 30000. */
|
|
940
|
+
maxReconnectDelay?: number;
|
|
931
941
|
/** 최대 재연결 시도. 기본 10. */
|
|
932
942
|
maxReconnectAttempts?: number;
|
|
943
|
+
/** 수신 하트비트 간격 (ms). 기본 10000. */
|
|
944
|
+
heartbeatIncoming?: number;
|
|
945
|
+
/** 발신 하트비트 간격 (ms). 기본 10000. */
|
|
946
|
+
heartbeatOutgoing?: number;
|
|
947
|
+
/**
|
|
948
|
+
* 탭 복귀(visibilitychange/pageshow) · 네트워크 복구(online) 시 자동 재연결. 기본 true.
|
|
949
|
+
*
|
|
950
|
+
* <p>재시도 횟수를 소진해 죽은 연결도 되살린다. 단 {@code connect()} 를 한 번도 호출하지 않았거나
|
|
951
|
+
* {@code disconnect()} 로 명시적으로 끊은 경우에는 동작하지 않는다.</p>
|
|
952
|
+
*/
|
|
953
|
+
resumeOnVisibility?: boolean;
|
|
954
|
+
/**
|
|
955
|
+
* 재연결/복귀 후 끊긴 구간의 메시지를 자동으로 다시 불러올지. 기본 true.
|
|
956
|
+
*
|
|
957
|
+
* <p>복구된 메시지는 일반 `chatMessage` 이벤트로 재생되므로 수신 코드를 바꿀 필요가 없다.</p>
|
|
958
|
+
*/
|
|
959
|
+
syncOnReconnect?: boolean;
|
|
960
|
+
/** 갭 복구 첫 페이지에만 적용되는 되감기 폭 (ms). 기본 3000. 서버가 상한 clamp 후 1회 차감. */
|
|
961
|
+
rewindMillis?: number;
|
|
962
|
+
/** 갭 복구 페이지 상한. 기본 5. 초과 시 최신 페이지 재로드로 전환하고 `gapTruncated` 를 발행. */
|
|
963
|
+
maxSyncPages?: number;
|
|
964
|
+
/** 갭 복구 페이지 크기. 기본 100. */
|
|
965
|
+
syncPageSize?: number;
|
|
933
966
|
/** WebRTC ICE 서버. */
|
|
934
967
|
iceServers?: RTCIceServer[];
|
|
935
968
|
/** connect() 시 채팅방 리스트 자동 구독. 기본 true. */
|
|
@@ -1229,7 +1262,7 @@ export interface RetentionCleanupPayload {
|
|
|
1229
1262
|
/**
|
|
1230
1263
|
* TalkFlowClient 이벤트 맵 — 런타임 {@code TalkFlowClient.js} 의 {@code this.emit(...)} 호출 기반.
|
|
1231
1264
|
*
|
|
1232
|
-
* <p><b>Connection 관련</b>: stateChange, connected, disconnected, reconnecting, connectionError, tokenSet, loggedOut</p>
|
|
1265
|
+
* <p><b>Connection 관련</b>: stateChange, connected, disconnected, reconnecting, reconnected, resumed, connectionError, tokenSet, loggedOut</p>
|
|
1233
1266
|
* <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
1267
|
* <p><b>Push 관련</b>: pushEnabled, pushFailed, pushNotification</p>
|
|
1235
1268
|
* <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 +1273,20 @@ export interface TalkFlowClientEvents {
|
|
|
1240
1273
|
connected: unknown;
|
|
1241
1274
|
disconnected: unknown;
|
|
1242
1275
|
reconnecting: { attempt: number };
|
|
1276
|
+
/**
|
|
1277
|
+
* 재연결 **성공**. 최초 연결에는 발행되지 않는다.
|
|
1278
|
+
*
|
|
1279
|
+
* <p>{@link TalkFlowClientEvents.resumed} 와 상호 배타적 — 재동기화 시점을 모두 잡으려면 둘 다 구독한다.</p>
|
|
1280
|
+
*/
|
|
1281
|
+
reconnected: { attempts: number };
|
|
1282
|
+
/**
|
|
1283
|
+
* 탭 복귀 / 네트워크 복구 감지 — **연결이 살아 있어 재연결이 불필요했던** 경우에만 발행된다.
|
|
1284
|
+
*
|
|
1285
|
+
* <p>백그라운드 동안 프레임을 놓쳤을 수 있으므로 소비자는 이 시점에도 재동기화해야 한다.
|
|
1286
|
+
* 복귀가 재연결을 유발한 경우에는 이 이벤트 대신 {@link TalkFlowClientEvents.reconnected} 가
|
|
1287
|
+
* 발행된다(두 이벤트가 겹치지 않아 재동기화가 중복 실행되지 않는다).</p>
|
|
1288
|
+
*/
|
|
1289
|
+
resumed: { reason: 'visibilitychange' | 'pageshow' | 'online' };
|
|
1243
1290
|
connectionError: unknown;
|
|
1244
1291
|
tokenSet: { userId: string };
|
|
1245
1292
|
loggedOut: void;
|
|
@@ -1288,6 +1335,21 @@ export interface TalkFlowClientEvents {
|
|
|
1288
1335
|
roomListRoomUpdated: RoomListUpdateEvent;
|
|
1289
1336
|
retentionCleanup: RetentionCleanupPayload;
|
|
1290
1337
|
|
|
1338
|
+
// 갭 복구 — 복구된 메시지 자체는 일반 chatMessage 로 재생되므로 아래는 모두 선택 구독용.
|
|
1339
|
+
/** 갭 복구 시작 (전 방 1회). */
|
|
1340
|
+
syncStarted: { roomIds: string[]; reason: string };
|
|
1341
|
+
/** 갭 복구 완료 (전 방 1회). */
|
|
1342
|
+
syncCompleted: {
|
|
1343
|
+
rooms: Array<{ roomId: string; recovered: number; truncated: boolean; durationMs: number }>;
|
|
1344
|
+
totalRecovered: number;
|
|
1345
|
+
durationMs: number;
|
|
1346
|
+
reason: string;
|
|
1347
|
+
};
|
|
1348
|
+
/** 갭이 `maxSyncPages` 를 넘거나 커서가 만료돼 최신 페이지 재로드로 전환됨 (방 단위). */
|
|
1349
|
+
gapTruncated: { roomId: string; fetched: number; reason?: 'cursorExpired' };
|
|
1350
|
+
/** 재연결 시 방 목록 전체 스냅샷. delta 계약인 `roomListUpdate` 와 별개. */
|
|
1351
|
+
roomListSynced: { rooms: ChatRoomResponse[]; reason: string };
|
|
1352
|
+
|
|
1291
1353
|
// Push
|
|
1292
1354
|
pushEnabled: void;
|
|
1293
1355
|
/**
|