@vibexnpm/talkx 3.0.7 → 3.1.0
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 +84 -14
- 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 +1 -1
- package/src/chat/ChatClient.js +9 -0
- package/src/chat/RoomUnreadReconciler.js +125 -0
- package/src/constants.js +8 -1
- package/src/index.js +2 -0
- package/src/talkflow/eventForwarding.js +1 -0
- package/types/index.d.ts +84 -14
package/package.json
CHANGED
package/src/chat/ChatClient.js
CHANGED
|
@@ -2451,6 +2451,15 @@ class ChatClient extends EventEmitter {
|
|
|
2451
2451
|
cutoffTime: event.cutoffTime
|
|
2452
2452
|
});
|
|
2453
2453
|
break;
|
|
2454
|
+
case RoomListEventType.ROOM_UNREAD_UPDATED:
|
|
2455
|
+
// 다른(또는 이) 기기에서 내가 읽어 방 미독이 바뀜 → badge 를 절대값으로 세팅.
|
|
2456
|
+
// roomUnreadCount 는 delta 가 아니라 절대값, unreadVersion 은 순서 역전 판별용.
|
|
2457
|
+
this.emit('roomListUnreadUpdated', {
|
|
2458
|
+
roomId: event.roomId,
|
|
2459
|
+
roomUnreadCount: event.roomUnreadCount,
|
|
2460
|
+
unreadVersion: event.unreadVersion
|
|
2461
|
+
});
|
|
2462
|
+
break;
|
|
2454
2463
|
default:
|
|
2455
2464
|
this.logger.warn('Unknown room list event type:', event.eventType);
|
|
2456
2465
|
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 방별 미독 카운트 계산기 (READ_SYNC_DESIGN v2.1 §6).
|
|
3
|
+
*
|
|
4
|
+
* <p>SDK 는 방 목록 상태를 소유하지 않는다(페이징·필터·프레임워크 상태와 충돌). 대신 순서 역전에
|
|
5
|
+
* 안전한 "카운트 계산"만 이 헬퍼로 제공하고, 앱이 결과를 자신의 room store 에 반영한다.</p>
|
|
6
|
+
*
|
|
7
|
+
* <p><b>왜 단순 절대값 세팅이 안 되나</b>: 서로 다른 발신 흐름에서 나온 이벤트는 도착 순서가 논리적
|
|
8
|
+
* 발생 순서와 다를 수 있다. 예컨대 내가 방을 읽어 나간 {@code ROOM_UNREAD_UPDATED(count=0, v=13)} 이
|
|
9
|
+
* 신규 {@code MESSAGE_RECEIVED(+1, v=12)} 보다 먼저 도착할 수 있다. 단순히 절대값을 덮으면 뒤늦게 온
|
|
10
|
+
* v12 delta 가 badge 를 1 로 되돌린다. 그래서 방마다 스냅샷(마지막 절대값)과 그 이후의 delta 를 분리
|
|
11
|
+
* 보관하고, version 으로 이미 반영된 delta 를 정리한다.</p>
|
|
12
|
+
*
|
|
13
|
+
* <p><b>방별 상태</b>: {@code snapshotCount}/{@code snapshotVersion}(마지막 절대값) +
|
|
14
|
+
* {@code pendingDeltas}(messageId → {version, delta}). 표시값 = snapshotCount + Σ pending.delta.</p>
|
|
15
|
+
*
|
|
16
|
+
* <p><b>버전 전제</b>: 순서 정합성은 서버가 이벤트/응답에 {@code version} 을 실어줄 때만 보장된다.
|
|
17
|
+
* version 이 아예 없는 구서버 구간에서는 순서 역전을 판별할 수 없다 — 이 경우 이미 versioned baseline 이
|
|
18
|
+
* 있으면 비교 불가한 legacy 스냅샷을 무시해 최신 pair 를 지키고, 최종 정합은 REST 재조회에 의존한다.</p>
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* const reconciler = new RoomUnreadReconciler(client.userId); // JWT 에서 SDK 가 추출한 내 userId
|
|
22
|
+
* // 초기 REST 페이지를 baseline 으로 투입
|
|
23
|
+
* rooms.forEach(r => reconciler.applySnapshot(r.id, r.unreadCount, r.unreadVersion));
|
|
24
|
+
* client.on('roomListMessage', e => { reconciler.applyMessageReceived(e); render(e.roomId); });
|
|
25
|
+
* client.on('roomListUnreadUpdated', e => { reconciler.applySnapshot(e.roomId, e.roomUnreadCount, e.unreadVersion); render(e.roomId); });
|
|
26
|
+
* function render(roomId) { store.setBadge(roomId, reconciler.getCount(roomId)); } // null 이면 REST 로 표시
|
|
27
|
+
*/
|
|
28
|
+
export class RoomUnreadReconciler {
|
|
29
|
+
/**
|
|
30
|
+
* @param {string} currentUserId - 내 사용자 ID. SDK 가 JWT 에서 추출한 값을 {@code client.userId} 로
|
|
31
|
+
* 얻는다(별도 관리 불필요). 내가 보낸 메시지의 MESSAGE_RECEIVED 가 내 badge 를 올리지 않도록 걸러내는
|
|
32
|
+
* 데만 쓰는 <b>표시용 값</b>이라 보안 결정에 관여하지 않는다. (서버도 발신자를 unread 대상에서 제외하지만,
|
|
33
|
+
* 방 리스트 이벤트는 발신자에게도 전달되므로 클라에서 한 번 더 거른다.)
|
|
34
|
+
*/
|
|
35
|
+
constructor(currentUserId) {
|
|
36
|
+
this._currentUserId = currentUserId;
|
|
37
|
+
/** @type {Map<string, {snapshotCount: (number|null), snapshotVersion: (number|null), pendingDeltas: Map<string, {version: (number|null), delta: number}>}>} */
|
|
38
|
+
this._rooms = new Map();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
_room(roomId) {
|
|
42
|
+
let state = this._rooms.get(roomId);
|
|
43
|
+
if (!state) {
|
|
44
|
+
state = { snapshotCount: null, snapshotVersion: null, pendingDeltas: new Map() };
|
|
45
|
+
this._rooms.set(roomId, state);
|
|
46
|
+
}
|
|
47
|
+
return state;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* MESSAGE_RECEIVED(delta +1) 반영.
|
|
52
|
+
* @param {{roomId: string, messageId: string, actorId?: string, unreadVersion?: (number|null)}} event
|
|
53
|
+
*/
|
|
54
|
+
applyMessageReceived(event) {
|
|
55
|
+
if (!event || !event.roomId || !event.messageId) return;
|
|
56
|
+
// 발신자 본인 메시지 → 내 badge 증가 금지.
|
|
57
|
+
if (event.actorId != null && event.actorId === this._currentUserId) return;
|
|
58
|
+
|
|
59
|
+
const state = this._room(event.roomId);
|
|
60
|
+
// 같은 메시지 중복 수신(서버측 멱등 race 복구 등) → 한 번만.
|
|
61
|
+
if (state.pendingDeltas.has(event.messageId)) return;
|
|
62
|
+
|
|
63
|
+
const version = event.unreadVersion == null ? null : event.unreadVersion;
|
|
64
|
+
// 이미 baseline 에 반영된 delta(스냅샷 version 이하) → 무시.
|
|
65
|
+
if (state.snapshotVersion != null && version != null && version <= state.snapshotVersion) return;
|
|
66
|
+
|
|
67
|
+
state.pendingDeltas.set(event.messageId, { version, delta: 1 });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 절대값 스냅샷 반영 — REST 응답 또는 ROOM_UNREAD_UPDATED.
|
|
72
|
+
* @param {string} roomId
|
|
73
|
+
* @param {(number|null|undefined)} count 방별 내 미독 절대값. null/undefined 면 무시(미계산 방).
|
|
74
|
+
* @param {(number|null|undefined)} version 그 카운트가 확정된 방 version.
|
|
75
|
+
*/
|
|
76
|
+
applySnapshot(roomId, count, version) {
|
|
77
|
+
if (!roomId || count == null) return;
|
|
78
|
+
const v = version == null ? null : version;
|
|
79
|
+
const state = this._room(roomId);
|
|
80
|
+
|
|
81
|
+
// 이미 versioned baseline 이 있는데 version 없는(구서버) 스냅샷이 오면 비교 불가 → 무시.
|
|
82
|
+
// 그대로 count 를 덮으면 snapshotVersion 은 최신인데 count 는 옛 값인 (옛 count, 최신 version) 조합이
|
|
83
|
+
// 되어, 백엔드가 원자 read 로 막은 순서 훼손이 SDK 에서 재현된다.
|
|
84
|
+
if (state.snapshotVersion != null && v == null) return;
|
|
85
|
+
|
|
86
|
+
// 더 오래된 스냅샷은 무시(stale).
|
|
87
|
+
if (state.snapshotVersion != null && v != null && v < state.snapshotVersion) return;
|
|
88
|
+
|
|
89
|
+
state.snapshotCount = count;
|
|
90
|
+
if (v != null) state.snapshotVersion = v;
|
|
91
|
+
|
|
92
|
+
// 이 스냅샷에 이미 반영된 pending 제거, 더 최신 pending 은 보존(v13 유실 방지).
|
|
93
|
+
// version 이 있을 때만 정리 가능 — version 없는 스냅샷(구서버)은 pending 을 건드리지 않는다.
|
|
94
|
+
if (v != null) {
|
|
95
|
+
for (const [messageId, d] of state.pendingDeltas) {
|
|
96
|
+
if (d.version == null || d.version <= v) {
|
|
97
|
+
state.pendingDeltas.delete(messageId);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 방의 현재 표시 카운트.
|
|
105
|
+
* @param {string} roomId
|
|
106
|
+
* @returns {(number|null)} baseline(스냅샷)이 아직 없으면 null — 앱은 REST 조회 결과로 표시한다.
|
|
107
|
+
*/
|
|
108
|
+
getCount(roomId) {
|
|
109
|
+
const state = this._rooms.get(roomId);
|
|
110
|
+
if (!state || state.snapshotCount == null) return null;
|
|
111
|
+
let sum = state.snapshotCount;
|
|
112
|
+
for (const d of state.pendingDeltas.values()) sum += d.delta;
|
|
113
|
+
return sum;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** 방을 상태에서 제거(퇴장/추방 등). */
|
|
117
|
+
removeRoom(roomId) {
|
|
118
|
+
this._rooms.delete(roomId);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** 전체 초기화(로그아웃/재연결 리셋 등). */
|
|
122
|
+
clear() {
|
|
123
|
+
this._rooms.clear();
|
|
124
|
+
}
|
|
125
|
+
}
|
package/src/constants.js
CHANGED
|
@@ -298,7 +298,14 @@ export const RoomListEventType = {
|
|
|
298
298
|
/** 방장에 의한 추방 + 영구 차단 → payload 는 ROOM_KICKED 와 동일, 재입장 거부됨. */
|
|
299
299
|
ROOM_BANNED: 'ROOM_BANNED',
|
|
300
300
|
/** 메시지 보관 기간 정리 → cutoffTime 이전 메시지를 UI에서 제거. */
|
|
301
|
-
MESSAGE_RETENTION_CLEANUP: 'MESSAGE_RETENTION_CLEANUP'
|
|
301
|
+
MESSAGE_RETENTION_CLEANUP: 'MESSAGE_RETENTION_CLEANUP',
|
|
302
|
+
/**
|
|
303
|
+
* 내 unread 가 변경됨 → 방 리스트 badge 를 roomUnreadCount 절대값으로 세팅(delta 아님).
|
|
304
|
+
* 내가 어느 기기에서 읽든 같은 사용자의 다른 세션에 전달돼 크로스-디바이스로 badge 를 맞춘다.
|
|
305
|
+
* unreadVersion 으로 순서 역전(읽음 0 이 신규 메시지보다 먼저 도착)을 방어한다.
|
|
306
|
+
* SDK 는 이를 roomListUnreadUpdated 이벤트로 emit 한다.
|
|
307
|
+
*/
|
|
308
|
+
ROOM_UNREAD_UPDATED: 'ROOM_UNREAD_UPDATED'
|
|
302
309
|
};
|
|
303
310
|
|
|
304
311
|
/**
|
package/src/index.js
CHANGED
|
@@ -84,6 +84,8 @@ export { default as ConnectionManager } from './core/ConnectionManager.js';
|
|
|
84
84
|
|
|
85
85
|
// 채팅
|
|
86
86
|
export { default as ChatClient } from './chat/ChatClient.js';
|
|
87
|
+
// 방 미독 계산기 — ROOM_UNREAD_UPDATED / MESSAGE_RECEIVED 를 순서 역전에 안전하게 합산(READ_SYNC_DESIGN §6).
|
|
88
|
+
export { RoomUnreadReconciler } from './chat/RoomUnreadReconciler.js';
|
|
87
89
|
|
|
88
90
|
// WebRTC
|
|
89
91
|
export { default as WebRTCClient } from './webrtc/WebRTCClient.js';
|
|
@@ -42,6 +42,7 @@ const CHAT_EVENT_MAP = [
|
|
|
42
42
|
['roomListBanned', 'roomListBanned'],
|
|
43
43
|
['roomListSelfBanned', 'roomListSelfBanned'],
|
|
44
44
|
['roomListRoomUpdated', 'roomListRoomUpdated'],
|
|
45
|
+
['roomListUnreadUpdated', 'roomListUnreadUpdated'],
|
|
45
46
|
['retentionCleanup', 'retentionCleanup']
|
|
46
47
|
];
|
|
47
48
|
|
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
|
/**
|
|
@@ -484,6 +484,7 @@ export const RoomListEventType: {
|
|
|
484
484
|
readonly ROOM_KICKED: 'ROOM_KICKED';
|
|
485
485
|
readonly ROOM_BANNED: 'ROOM_BANNED';
|
|
486
486
|
readonly MESSAGE_RETENTION_CLEANUP: 'MESSAGE_RETENTION_CLEANUP';
|
|
487
|
+
readonly ROOM_UNREAD_UPDATED: 'ROOM_UNREAD_UPDATED';
|
|
487
488
|
};
|
|
488
489
|
export type RoomListEventTypeValue = typeof RoomListEventType[keyof typeof RoomListEventType];
|
|
489
490
|
|
|
@@ -723,7 +724,8 @@ export interface ParticipantResponse {
|
|
|
723
724
|
* 과도기 동안 {@code ADMIN} 생성자가 보일 수 있으나 권한은 동일.</p>
|
|
724
725
|
*/
|
|
725
726
|
role: 'OWNER' | 'ADMIN' | 'MEMBER' | 'ASSISTANT';
|
|
726
|
-
|
|
727
|
+
/** 참가 시각(epoch millis). */
|
|
728
|
+
joinedAt: number;
|
|
727
729
|
}
|
|
728
730
|
|
|
729
731
|
/** 채팅방 last message preview. */
|
|
@@ -734,7 +736,8 @@ export interface LastMessageResponse {
|
|
|
734
736
|
senderProfileUrl?: string | null;
|
|
735
737
|
content: string;
|
|
736
738
|
messageType: string;
|
|
737
|
-
|
|
739
|
+
/** 마지막 메시지 시각(epoch millis). */
|
|
740
|
+
sentAt: number;
|
|
738
741
|
/**
|
|
739
742
|
* 파일 메시지일 때 첫 파일의 사용자 metadata (opaque pass-through).
|
|
740
743
|
* <p>고객사가 파일 업로드 시 함께 보낸 데이터 (스티커 ID 등) 가 채팅방 리스트에서도 식별 가능하도록 보존.
|
|
@@ -764,6 +767,11 @@ export interface ChatRoomResponse {
|
|
|
764
767
|
description?: string;
|
|
765
768
|
creatorId: string;
|
|
766
769
|
unreadCount?: number | null;
|
|
770
|
+
/**
|
|
771
|
+
* unreadCount 가 확정된 방 version — {@link RoomUnreadReconciler} 의 baseline.
|
|
772
|
+
* 미참여 방 등 unread 미계산 응답 또는 구버전 서버에서는 없을 수 있음.
|
|
773
|
+
*/
|
|
774
|
+
unreadVersion?: number | null;
|
|
767
775
|
isParticipating?: boolean | null;
|
|
768
776
|
activeParticipantCount: number;
|
|
769
777
|
messageRetentionHours?: number | null;
|
|
@@ -771,8 +779,10 @@ export interface ChatRoomResponse {
|
|
|
771
779
|
pinnedMessageId?: string | null;
|
|
772
780
|
participants?: ParticipantResponse[];
|
|
773
781
|
lastMessage?: LastMessageResponse | null;
|
|
774
|
-
|
|
775
|
-
|
|
782
|
+
/** 마지막 메시지 시각(epoch millis) — 방 목록 정렬용. */
|
|
783
|
+
lastMessageAt?: number | null;
|
|
784
|
+
/** 방 생성 시각(epoch millis). */
|
|
785
|
+
createdAt: number;
|
|
776
786
|
}
|
|
777
787
|
|
|
778
788
|
/** 커서 페이지네이션 응답. */
|
|
@@ -825,6 +835,10 @@ export interface RoomListUpdateEvent {
|
|
|
825
835
|
eventType: RoomListEventTypeValue;
|
|
826
836
|
roomId: string;
|
|
827
837
|
actorId?: string;
|
|
838
|
+
/** {@code MESSAGE_*} 이벤트의 방 마지막 메시지 시각(epoch millis). 그 외 이벤트에서는 undefined. */
|
|
839
|
+
lastMessageAt?: number | null;
|
|
840
|
+
/** 이벤트 발행 시각(epoch millis). */
|
|
841
|
+
timestamp?: number;
|
|
828
842
|
cutoffTime?: number;
|
|
829
843
|
/** {@code ROOM_UPDATED} 전용 — 저장된 최신 메타 snapshot. 다른 eventType 에서는 undefined. */
|
|
830
844
|
room?: RoomMeta;
|
|
@@ -843,9 +857,32 @@ export interface RoomListUpdateEvent {
|
|
|
843
857
|
* SDK 내부에서 같은 메시지의 중복 수신 (서버측 멱등 race 복구 등) dedup 용도. 다른 eventType 에서는 undefined.
|
|
844
858
|
*/
|
|
845
859
|
messageId?: string;
|
|
860
|
+
/**
|
|
861
|
+
* {@code ROOM_UNREAD_UPDATED} 전용 — 방별 내 미독 메시지 수 <b>절대값</b>(delta 아님).
|
|
862
|
+
* 다른 eventType 에서는 undefined. 전용 이벤트 {@link RoomUnreadUpdatedEvent} 로도 받을 수 있다.
|
|
863
|
+
*/
|
|
864
|
+
roomUnreadCount?: number;
|
|
865
|
+
/**
|
|
866
|
+
* unread 를 바꾼 원자 연산이 확정한 방 단위 version. {@code MESSAGE_RECEIVED} 와
|
|
867
|
+
* {@code ROOM_UNREAD_UPDATED} 에 실려, 순서 역전을 {@link RoomUnreadReconciler} 가 판별한다.
|
|
868
|
+
*/
|
|
869
|
+
unreadVersion?: number;
|
|
846
870
|
[extra: string]: unknown;
|
|
847
871
|
}
|
|
848
872
|
|
|
873
|
+
/**
|
|
874
|
+
* `roomListUnreadUpdated` 이벤트 payload — 내가 어느 기기에서 읽든 같은 사용자의 다른 세션에 전달된다.
|
|
875
|
+
*
|
|
876
|
+
* <p>{@link RoomUnreadReconciler.applySnapshot} 에 그대로 넣으면 순서 역전에 안전하게 badge 를 계산할 수 있다.</p>
|
|
877
|
+
*/
|
|
878
|
+
export interface RoomUnreadUpdatedEvent {
|
|
879
|
+
roomId: string;
|
|
880
|
+
/** 방별 내 미독 메시지 수 절대값. */
|
|
881
|
+
roomUnreadCount: number;
|
|
882
|
+
/** 그 카운트가 확정된 방 version. */
|
|
883
|
+
unreadVersion: number;
|
|
884
|
+
}
|
|
885
|
+
|
|
849
886
|
/** 포그라운드 푸시 알림 payload. */
|
|
850
887
|
export interface PushNotificationPayload {
|
|
851
888
|
title: string;
|
|
@@ -1325,6 +1362,8 @@ export interface TalkFlowClientEvents {
|
|
|
1325
1362
|
/** 본인이 영구 차단당함. */
|
|
1326
1363
|
roomListSelfBanned: RoomListUpdateEvent;
|
|
1327
1364
|
roomListRoomUpdated: RoomListUpdateEvent;
|
|
1365
|
+
/** 내가 어느 기기에서 읽든 방별 미독 절대값(+version)을 통지 — 크로스-디바이스 badge 동기화. */
|
|
1366
|
+
roomListUnreadUpdated: RoomUnreadUpdatedEvent;
|
|
1328
1367
|
retentionCleanup: RetentionCleanupPayload;
|
|
1329
1368
|
|
|
1330
1369
|
// 갭 복구 — 복구된 메시지 자체는 일반 chatMessage 로 재생되므로 아래는 모두 선택 구독용.
|
|
@@ -1426,6 +1465,8 @@ export interface ChatClientEvents {
|
|
|
1426
1465
|
/** 본인이 영구 차단당함. */
|
|
1427
1466
|
roomListSelfBanned: RoomListUpdateEvent;
|
|
1428
1467
|
roomListRoomUpdated: RoomListUpdateEvent;
|
|
1468
|
+
/** 내가 어느 기기에서 읽든 방별 미독 절대값(+version)을 통지 — 크로스-디바이스 badge 동기화. */
|
|
1469
|
+
roomListUnreadUpdated: RoomUnreadUpdatedEvent;
|
|
1429
1470
|
retentionCleanup: RetentionCleanupPayload;
|
|
1430
1471
|
messageRead: MessageReadEventPayload;
|
|
1431
1472
|
typing: TypingEvent;
|
|
@@ -2013,3 +2054,32 @@ export class Logger {
|
|
|
2013
2054
|
export class ConnectionManager {}
|
|
2014
2055
|
export class MediaStreamManager {}
|
|
2015
2056
|
export class PeerConnectionManager {}
|
|
2057
|
+
|
|
2058
|
+
/**
|
|
2059
|
+
* 방 미독 카운트 계산기 (READ_SYNC_DESIGN §6).
|
|
2060
|
+
*
|
|
2061
|
+
* <p>SDK 는 방 목록 상태를 소유하지 않는다 — 순서 역전에 안전한 카운트 계산만 제공하고 앱이 결과를
|
|
2062
|
+
* 자신의 room store 에 반영한다. 방마다 마지막 절대값 스냅샷과 그 이후 delta 를 분리 보관해,
|
|
2063
|
+
* 서로 다른 발신 흐름의 이벤트가 도착 순서가 뒤바뀌어도 서버 진실과 수렴한다.</p>
|
|
2064
|
+
*
|
|
2065
|
+
* @example
|
|
2066
|
+
* const reconciler = new RoomUnreadReconciler(client.userId); // JWT 에서 SDK 가 추출한 내 userId
|
|
2067
|
+
* rooms.forEach(r => reconciler.applySnapshot(r.id, r.unreadCount, r.unreadVersion)); // REST baseline
|
|
2068
|
+
* client.on('roomListMessage', e => { reconciler.applyMessageReceived(e); render(e.roomId); });
|
|
2069
|
+
* client.on('roomListUnreadUpdated', e => { reconciler.applySnapshot(e.roomId, e.roomUnreadCount, e.unreadVersion); render(e.roomId); });
|
|
2070
|
+
* // render: const c = reconciler.getCount(roomId); c == null ? useRestValue() : store.setBadge(roomId, c);
|
|
2071
|
+
*/
|
|
2072
|
+
export class RoomUnreadReconciler {
|
|
2073
|
+
/** @param currentUserId 내 사용자 ID — 내가 보낸 메시지의 delta 를 걸러내는 데 쓴다. */
|
|
2074
|
+
constructor(currentUserId: string);
|
|
2075
|
+
/** MESSAGE_RECEIVED(delta +1) 반영. 발신자 본인 · 중복 messageId · 이미 반영된 version 은 무시. */
|
|
2076
|
+
applyMessageReceived(event: { roomId: string; messageId: string; actorId?: string; unreadVersion?: number | null }): void;
|
|
2077
|
+
/** 절대값 스냅샷(REST 또는 ROOM_UNREAD_UPDATED) 반영. stale version 은 무시, 반영된 pending 은 정리. */
|
|
2078
|
+
applySnapshot(roomId: string, count: number | null | undefined, version: number | null | undefined): void;
|
|
2079
|
+
/** 방의 현재 표시 카운트. baseline 이 아직 없으면 null(앱은 REST 값으로 표시). */
|
|
2080
|
+
getCount(roomId: string): number | null;
|
|
2081
|
+
/** 방을 상태에서 제거. */
|
|
2082
|
+
removeRoom(roomId: string): void;
|
|
2083
|
+
/** 전체 초기화. */
|
|
2084
|
+
clear(): void;
|
|
2085
|
+
}
|