@vibexnpm/talkx 2.3.1
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/README.md +327 -0
- package/dist/index.d.ts +1771 -0
- package/dist/talkflow-sdk.esm.js +2 -0
- package/dist/talkflow-sdk.esm.js.map +1 -0
- package/dist/talkflow-sdk.standalone.js +2 -0
- package/dist/talkflow-sdk.standalone.js.map +1 -0
- package/dist/talkflow-sdk.umd.js +2 -0
- package/dist/talkflow-sdk.umd.js.map +1 -0
- package/package.json +51 -0
- package/src/TalkFlowClient.js +481 -0
- package/src/chat/ChatClient.js +2221 -0
- package/src/constants.js +411 -0
- package/src/core/ConnectionManager.js +517 -0
- package/src/index.js +97 -0
- package/src/push/PushManager.js +893 -0
- package/src/talkflow/delegates.js +112 -0
- package/src/talkflow/eventForwarding.js +93 -0
- package/src/talkflow/session.js +355 -0
- package/src/utils/ApiClient.js +305 -0
- package/src/utils/EventEmitter.js +113 -0
- package/src/utils/Logger.js +88 -0
- package/src/utils/jwtUtils.js +213 -0
- package/src/webrtc/MediaStreamManager.js +478 -0
- package/src/webrtc/PeerConnectionManager.js +467 -0
- package/src/webrtc/WebRTCClient.js +1041 -0
- package/types/index.d.ts +1771 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
const CHAT_DELEGATE_METHODS = [
|
|
2
|
+
'getRooms',
|
|
3
|
+
'getRoom',
|
|
4
|
+
'getRoomInfo',
|
|
5
|
+
'setMyRoomLanguage',
|
|
6
|
+
'createOneToOneRoom',
|
|
7
|
+
'createGroupRoom',
|
|
8
|
+
'joinGroupRoom',
|
|
9
|
+
'leaveRoom',
|
|
10
|
+
'updateGroupRoom',
|
|
11
|
+
'inviteToGroupRoom',
|
|
12
|
+
'kickMember',
|
|
13
|
+
'banMember',
|
|
14
|
+
'unbanMember',
|
|
15
|
+
'getBannedMembers',
|
|
16
|
+
'getAvailableGroupRooms',
|
|
17
|
+
'getAllGroupRooms',
|
|
18
|
+
'enterRoom',
|
|
19
|
+
'getMessages',
|
|
20
|
+
'fetchLinkPreview',
|
|
21
|
+
'sendMessage',
|
|
22
|
+
'sendMessageOptimistic',
|
|
23
|
+
'sendTextMessage',
|
|
24
|
+
'sendTextMessageOptimistic',
|
|
25
|
+
'sendReply',
|
|
26
|
+
'sendReplyOptimistic',
|
|
27
|
+
'uploadFile',
|
|
28
|
+
'sendFileMessage',
|
|
29
|
+
'sendFileMessageOptimistic',
|
|
30
|
+
'editMessage',
|
|
31
|
+
'deleteMessage',
|
|
32
|
+
'markAsRead',
|
|
33
|
+
'pinMessage',
|
|
34
|
+
'unpinMessage',
|
|
35
|
+
'toggleReaction',
|
|
36
|
+
'subscribeRoom',
|
|
37
|
+
'unsubscribeRoom',
|
|
38
|
+
'unsubscribeAllRooms',
|
|
39
|
+
'setActiveRoom',
|
|
40
|
+
'clearActiveRoom',
|
|
41
|
+
'getActiveRoom',
|
|
42
|
+
'subscribeRoomList',
|
|
43
|
+
'unsubscribeRoomList',
|
|
44
|
+
'isRoomListSubscribed',
|
|
45
|
+
'getSubscribedRooms',
|
|
46
|
+
'isSubscribed',
|
|
47
|
+
'startTyping',
|
|
48
|
+
'stopTyping',
|
|
49
|
+
'getAssistants',
|
|
50
|
+
'getRoomAiMeta',
|
|
51
|
+
'rateAssistantMessage',
|
|
52
|
+
'summarizeWithAssistant',
|
|
53
|
+
'translateWithAssistant',
|
|
54
|
+
'getRoomPmPrompt',
|
|
55
|
+
'upsertRoomPmPrompt',
|
|
56
|
+
'activateRoomPmPrompt',
|
|
57
|
+
'deactivateRoomPmPrompt',
|
|
58
|
+
'getRoomPmPromptVersions',
|
|
59
|
+
'activateRoomPmPromptVersion',
|
|
60
|
+
'previewRoomPmPrompt'
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
const WEBRTC_DELEGATE_METHODS = [
|
|
64
|
+
'initializeIceServers',
|
|
65
|
+
'getTurnCredentials',
|
|
66
|
+
'createCallRoom',
|
|
67
|
+
'getCallRoom',
|
|
68
|
+
'joinCallRoomApi',
|
|
69
|
+
'leaveCallRoomApi',
|
|
70
|
+
'enableIncomingCalls',
|
|
71
|
+
'disableIncomingCalls',
|
|
72
|
+
'isIncomingCallsEnabled',
|
|
73
|
+
'startCall',
|
|
74
|
+
'callUser',
|
|
75
|
+
'acceptCall',
|
|
76
|
+
'rejectCall',
|
|
77
|
+
'cancelCall',
|
|
78
|
+
'endCall',
|
|
79
|
+
'toggleVideo',
|
|
80
|
+
'toggleAudio',
|
|
81
|
+
'setVideoEnabled',
|
|
82
|
+
'setAudioEnabled',
|
|
83
|
+
'startScreenShare',
|
|
84
|
+
'stopScreenShare',
|
|
85
|
+
'getLocalStream',
|
|
86
|
+
'getDevices',
|
|
87
|
+
'switchDevice',
|
|
88
|
+
'startDeviceChangeDetection',
|
|
89
|
+
'stopDeviceChangeDetection',
|
|
90
|
+
'applyVideoConstraints',
|
|
91
|
+
'getVideoSettings',
|
|
92
|
+
'getAudioSettings',
|
|
93
|
+
'isInCall',
|
|
94
|
+
'getCurrentRoom',
|
|
95
|
+
'getParticipants',
|
|
96
|
+
'getMediaState',
|
|
97
|
+
'getConnectionSummary',
|
|
98
|
+
'getStats'
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
function defineDelegates(prototype, targetKey, methods) {
|
|
102
|
+
methods.forEach((method) => {
|
|
103
|
+
prototype[method] = function delegatedMethod(...args) {
|
|
104
|
+
return this[targetKey][method](...args);
|
|
105
|
+
};
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function applyDelegatedMethods(TalkFlowClient) {
|
|
110
|
+
defineDelegates(TalkFlowClient.prototype, 'chat', CHAT_DELEGATE_METHODS);
|
|
111
|
+
defineDelegates(TalkFlowClient.prototype, 'webrtc', WEBRTC_DELEGATE_METHODS);
|
|
112
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const CONNECTION_EVENT_MAP = [
|
|
2
|
+
['connected', 'connected'],
|
|
3
|
+
['disconnected', 'disconnected'],
|
|
4
|
+
['reconnecting', 'reconnecting'],
|
|
5
|
+
['error', 'connectionError']
|
|
6
|
+
];
|
|
7
|
+
|
|
8
|
+
const CHAT_EVENT_MAP = [
|
|
9
|
+
['message', 'chatMessage'],
|
|
10
|
+
['newMessage', 'newChatMessage'],
|
|
11
|
+
['messageUpdated', 'messageUpdated'],
|
|
12
|
+
['messageDeleted', 'messageDeleted'],
|
|
13
|
+
['reactionChanged', 'reactionChanged'],
|
|
14
|
+
['linkPreviewAttached', 'linkPreviewAttached'],
|
|
15
|
+
['messageTranslated', 'messageTranslated'],
|
|
16
|
+
['messageRead', 'messageRead'],
|
|
17
|
+
['typing', 'typing'],
|
|
18
|
+
['memberJoined', 'memberJoined'],
|
|
19
|
+
['memberLeft', 'memberLeft'],
|
|
20
|
+
['roomSubscribed', 'roomSubscribed'],
|
|
21
|
+
['roomUnsubscribed', 'roomUnsubscribed'],
|
|
22
|
+
['roomListSubscribed', 'roomListSubscribed'],
|
|
23
|
+
['roomListUnsubscribed', 'roomListUnsubscribed'],
|
|
24
|
+
['roomListUpdate', 'roomListUpdate'],
|
|
25
|
+
['roomListMessage', 'roomListMessage'],
|
|
26
|
+
['roomListCreated', 'roomListCreated'],
|
|
27
|
+
['roomListJoined', 'roomListJoined'],
|
|
28
|
+
['roomListLeft', 'roomListLeft'],
|
|
29
|
+
['roomListSelfLeft', 'roomListSelfLeft'],
|
|
30
|
+
['roomListKicked', 'roomListKicked'],
|
|
31
|
+
['roomListSelfKicked', 'roomListSelfKicked'],
|
|
32
|
+
['roomListBanned', 'roomListBanned'],
|
|
33
|
+
['roomListSelfBanned', 'roomListSelfBanned'],
|
|
34
|
+
['roomListRoomUpdated', 'roomListRoomUpdated'],
|
|
35
|
+
['retentionCleanup', 'retentionCleanup']
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
const WEBRTC_EVENT_MAP = [
|
|
39
|
+
['localStreamStarted', 'localStreamStarted'],
|
|
40
|
+
['localStreamStopped', 'localStreamStopped'],
|
|
41
|
+
['remoteTrack', 'remoteTrack'],
|
|
42
|
+
['screenShareStarted', 'screenShareStarted'],
|
|
43
|
+
['screenShareEnded', 'screenShareEnded'],
|
|
44
|
+
['deviceChange', 'deviceChange'],
|
|
45
|
+
['mediaStateChanged', 'mediaStateChanged'],
|
|
46
|
+
['callStarted', 'callStarted'],
|
|
47
|
+
['callEnded', 'callEnded'],
|
|
48
|
+
['callRequested', 'callRequested'],
|
|
49
|
+
['callAccepted', 'callAccepted'],
|
|
50
|
+
['callRejected', 'callRejected'],
|
|
51
|
+
['callCancelled', 'callCancelled'],
|
|
52
|
+
['callBusy', 'callBusy'],
|
|
53
|
+
['incomingCall', 'incomingCall'],
|
|
54
|
+
['incomingCallWhileBusy', 'incomingCallWhileBusy'],
|
|
55
|
+
['callInvitation', 'callInvitation'],
|
|
56
|
+
['userJoined', 'userJoined'],
|
|
57
|
+
['userLeft', 'userLeft'],
|
|
58
|
+
['participantLeft', 'participantLeft'],
|
|
59
|
+
['participantMediaState', 'participantMediaState'],
|
|
60
|
+
['peerConnected', 'peerConnected'],
|
|
61
|
+
['peerDisconnected', 'peerDisconnected'],
|
|
62
|
+
['peerClosed', 'peerClosed'],
|
|
63
|
+
['error', 'webrtcError']
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
function forwardMappedEvents(source, target, eventMap) {
|
|
67
|
+
eventMap.forEach(([sourceEvent, targetEvent]) => {
|
|
68
|
+
source.on(sourceEvent, (data) => {
|
|
69
|
+
target.emit(targetEvent, data);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function setupEventForwarding(client) {
|
|
75
|
+
if (!client.connectionManager) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
client.connectionManager.on('stateChange', ({ state, prevState }) => {
|
|
80
|
+
client._state = state;
|
|
81
|
+
client.emit('stateChange', { state, prevState });
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
forwardMappedEvents(client.connectionManager, client, CONNECTION_EVENT_MAP);
|
|
85
|
+
|
|
86
|
+
if (client.chat) {
|
|
87
|
+
forwardMappedEvents(client.chat, client, CHAT_EVENT_MAP);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (client.webrtc) {
|
|
91
|
+
forwardMappedEvents(client.webrtc, client, WEBRTC_EVENT_MAP);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import { validateAndParseJWT, isJWTExpired } from '../utils/jwtUtils.js';
|
|
2
|
+
import ConnectionManager from '../core/ConnectionManager.js';
|
|
3
|
+
import ChatClient from '../chat/ChatClient.js';
|
|
4
|
+
import WebRTCClient from '../webrtc/WebRTCClient.js';
|
|
5
|
+
import PushManager from '../push/PushManager.js';
|
|
6
|
+
|
|
7
|
+
export function initializeSubClients(client) {
|
|
8
|
+
if (client.connectionManager) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (!client.userId) {
|
|
13
|
+
throw new Error('userId is required to initialize sub-clients. Please set JWT token first.');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
client.connectionManager = new ConnectionManager({
|
|
17
|
+
serverUrl: client.options.serverUrl,
|
|
18
|
+
jwtToken: client.options.jwtToken,
|
|
19
|
+
apiKey: client.options.apiKey,
|
|
20
|
+
projectId: client.options.projectId,
|
|
21
|
+
useSockJS: client.options.useSockJS,
|
|
22
|
+
reconnectDelay: client.options.reconnectDelay,
|
|
23
|
+
maxReconnectAttempts: client.options.maxReconnectAttempts,
|
|
24
|
+
logLevel: client.options.logLevel
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
client._chat = new ChatClient({
|
|
28
|
+
connectionManager: client.connectionManager,
|
|
29
|
+
apiClient: client.apiClient,
|
|
30
|
+
userId: client._userId,
|
|
31
|
+
logLevel: client.options.logLevel
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
client._webrtc = new WebRTCClient({
|
|
35
|
+
connectionManager: client.connectionManager,
|
|
36
|
+
apiClient: client.apiClient,
|
|
37
|
+
userId: client._userId,
|
|
38
|
+
iceServers: client.options.iceServers,
|
|
39
|
+
logLevel: client.options.logLevel
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
client._setupEventForwarding();
|
|
43
|
+
client.logger.debug('Sub-clients initialized');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function applySessionMethods(TalkFlowClient) {
|
|
47
|
+
TalkFlowClient.prototype.connect = async function connect(jwt, options = {}) {
|
|
48
|
+
const { enablePush = false } = options;
|
|
49
|
+
|
|
50
|
+
if (jwt) {
|
|
51
|
+
await this.setToken(jwt);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!this.options.jwtToken) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
'JWT token is required. Obtain it from your backend (which calls ' +
|
|
57
|
+
'POST /api/v1/users/auth with a Server API Key) and pass it to the SDK ' +
|
|
58
|
+
'via the jwtToken option, setToken(), or connect(jwt). See README "프로덕션 인증 플로우".'
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (isJWTExpired(this.options.jwtToken)) {
|
|
63
|
+
throw new Error('JWT token has expired. Please update the token before connecting.');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!this.connectionManager) {
|
|
67
|
+
this._initializeSubClients();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
await this.connectionManager.connect();
|
|
71
|
+
|
|
72
|
+
if (this.webrtc) {
|
|
73
|
+
await this.webrtc.enableIncomingCalls();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (this.chat && this.options.autoSubscribeRoomList) {
|
|
77
|
+
try {
|
|
78
|
+
await this.chat.subscribeRoomList();
|
|
79
|
+
} catch (error) {
|
|
80
|
+
this.logger.warn('Failed to auto-subscribe room list (non-fatal):', error);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (enablePush) {
|
|
85
|
+
this.enablePushNotifications();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
this.logger.info('Connected to server');
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
TalkFlowClient.prototype.disconnect = async function disconnect() {
|
|
92
|
+
if (!this.connectionManager) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (this.webrtc && this.webrtc.isInCall()) {
|
|
97
|
+
this.webrtc.endCall();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (this.webrtc) {
|
|
101
|
+
this.webrtc.disableIncomingCalls();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (this.chat) {
|
|
105
|
+
this.chat.unsubscribeAllRooms();
|
|
106
|
+
this.chat.unsubscribeRoomList();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
await this.connectionManager.disconnect();
|
|
110
|
+
this.logger.info('Disconnected from server');
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
TalkFlowClient.prototype.logout = async function logout() {
|
|
114
|
+
try {
|
|
115
|
+
await this.apiClient.post('/v1/auth/signout');
|
|
116
|
+
this.logger.info('Logged out from server');
|
|
117
|
+
} catch (error) {
|
|
118
|
+
this.logger.warn('Server logout failed (proceeding with local cleanup):', error.message);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
await this.disconnect();
|
|
122
|
+
|
|
123
|
+
if (this.pushManager) {
|
|
124
|
+
this.pushManager.reset();
|
|
125
|
+
this._pushEnablePromise = null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
this.options.jwtToken = null;
|
|
129
|
+
this._userId = null;
|
|
130
|
+
this.apiClient.setJwtToken(null);
|
|
131
|
+
|
|
132
|
+
this.emit('loggedOut');
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 웹 푸시 알림을 활성화한다.
|
|
137
|
+
*
|
|
138
|
+
* <p>전체 흐름: Firebase 초기화 → 서비스 워커 등록 → 권한 요청 → FCM 토큰 발급 → 서버 등록 →
|
|
139
|
+
* 포그라운드 메시지 핸들러 등록 → SW 에 projectId 등록.</p>
|
|
140
|
+
*
|
|
141
|
+
* <h3>호출 패턴</h3>
|
|
142
|
+
* <ul>
|
|
143
|
+
* <li><b>자동 활성화 (비차단)</b> — {@code client.enablePushNotifications();} 처럼
|
|
144
|
+
* await 없이 호출하면 fire-and-forget 으로 동작한다. 메인 흐름 (UI 렌더링 등) 을 차단하지 않는다.
|
|
145
|
+
* 이 경우 결과는 {@code 'pushEnabled'} / {@code 'pushFailed'} 이벤트로 받는다.
|
|
146
|
+
* <b>중요</b>: 이 패턴을 쓸 때는 호출 <b>전에</b> 이벤트 리스너를 등록해야 한다 — 그렇지 않으면
|
|
147
|
+
* 즉시 emit 되는 실패 이벤트를 놓칠 수 있다.</li>
|
|
148
|
+
* <li><b>사용자 액션 (await)</b> — 버튼 클릭 등 사용자 명시적 액션에서는
|
|
149
|
+
* {@code const result = await client.enablePushNotifications();} 으로 결과를 즉시 받아서
|
|
150
|
+
* UI 분기에 사용한다. throw 하지 않으므로 try/catch 불필요.</li>
|
|
151
|
+
* </ul>
|
|
152
|
+
*
|
|
153
|
+
* <p>이 메서드는 throw 하지 않는다. 실패는 {@code { ok: false, reason, error }} 형태의 객체로 반환된다.
|
|
154
|
+
* 따라서 fire-and-forget 으로 호출해도 unhandled rejection 이 발생하지 않는다.</p>
|
|
155
|
+
*
|
|
156
|
+
* @param {Object} [options]
|
|
157
|
+
* @param {Object} [options.firebaseConfig] - 커스텀 Firebase 설정 (테스트/검증용)
|
|
158
|
+
* @param {string} [options.vapidKey] - 커스텀 VAPID 키
|
|
159
|
+
* @param {string} [options.serviceWorkerPath] - 서비스 워커 경로
|
|
160
|
+
* @returns {Promise<{ok: true, alreadyEnabled?: boolean} | {ok: false, reason: string, error: Error}>}
|
|
161
|
+
* 성공 시 {@code { ok: true }}, 실패 시 {@code { ok: false, reason, error }}.
|
|
162
|
+
* {@code reason} 은 {@link PushErrorCode} 값 또는 {@code 'NO_TOKEN'}.
|
|
163
|
+
*/
|
|
164
|
+
TalkFlowClient.prototype.enablePushNotifications = async function enablePushNotifications(options = {}) {
|
|
165
|
+
// JWT 토큰 미설정 시 명확한 분류 에러로 통보 (throw 하지 않고 결과 객체 반환).
|
|
166
|
+
if (!this.options.jwtToken) {
|
|
167
|
+
const error = new Error('JWT token is required to enable push notifications.');
|
|
168
|
+
this.emit('pushFailed', { reason: 'NO_TOKEN', error });
|
|
169
|
+
return { ok: false, reason: 'NO_TOKEN', error };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!this.pushManager) {
|
|
173
|
+
this._pushManager = new PushManager({
|
|
174
|
+
apiClient: this.apiClient,
|
|
175
|
+
projectId: this.options.projectId,
|
|
176
|
+
firebaseConfig: options.firebaseConfig,
|
|
177
|
+
vapidKey: options.vapidKey,
|
|
178
|
+
serviceWorkerPath: options.serviceWorkerPath,
|
|
179
|
+
logLevel: this.options.logLevel
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// 포그라운드 메시지 콜백 — projectId 필터링 + activeRoom suppress + messageId dedup 후 client emit.
|
|
183
|
+
// PushManager 자체는 EventEmitter 가 아니므로 콜백 오버라이드로 우회한다.
|
|
184
|
+
//
|
|
185
|
+
// dedup: 같은 messageId 의 푸시가 서버측 멱등 race 복구 / FCM 재전송 등으로 중복 도착할 수 있어
|
|
186
|
+
// 5분 TTL Map 으로 한 번만 emit. setTimeout 으로 자동 cleanup.
|
|
187
|
+
const PUSH_DEDUP_TTL_MS = 5 * 60 * 1000;
|
|
188
|
+
this.pushManager._onForegroundMessage = (payload) => {
|
|
189
|
+
const data = payload.data || {};
|
|
190
|
+
|
|
191
|
+
if (data.projectId && data.projectId !== this.options.projectId) {
|
|
192
|
+
this.logger.debug('다른 프로젝트 푸시 무시:', data.projectId);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const activeRoomId = this.chat?.getActiveRoom();
|
|
197
|
+
|
|
198
|
+
if (activeRoomId && activeRoomId === data.roomId) {
|
|
199
|
+
this.logger.debug('포그라운드 푸시 suppress (현재 방):', data.roomId);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// messageId 기반 dedup — 키가 없는 시스템 푸시 등은 통과.
|
|
204
|
+
if (data.messageId) {
|
|
205
|
+
if (!this._recentPushMessageIds) {
|
|
206
|
+
this._recentPushMessageIds = new Map();
|
|
207
|
+
}
|
|
208
|
+
if (this._recentPushMessageIds.has(data.messageId)) {
|
|
209
|
+
this.logger.debug('포그라운드 푸시 dedup:', data.messageId);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const timer = setTimeout(() => {
|
|
213
|
+
this._recentPushMessageIds.delete(data.messageId);
|
|
214
|
+
}, PUSH_DEDUP_TTL_MS);
|
|
215
|
+
this._recentPushMessageIds.set(data.messageId, timer);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
this.emit('pushNotification', {
|
|
219
|
+
title: payload.notification?.title || data.title,
|
|
220
|
+
body: payload.notification?.body || data.body,
|
|
221
|
+
data
|
|
222
|
+
});
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// 가드 1: 이미 활성화됨 — 중복 호출은 성공으로 간주.
|
|
227
|
+
if (this.pushManager.isEnabled()) {
|
|
228
|
+
this.logger.debug('웹 푸시가 이미 활성화되어 있어 중복 호출을 건너뜁니다.');
|
|
229
|
+
return { ok: true, alreadyEnabled: true };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// 가드 2: 진행 중 — 같은 Promise 를 공유해 두 호출자가 동일 결과를 받도록 한다.
|
|
233
|
+
if (this._pushEnablePromise) {
|
|
234
|
+
this.logger.debug('웹 푸시 활성화가 이미 진행 중입니다.');
|
|
235
|
+
return this._pushEnablePromise;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// 본 활성화 흐름 — throw 하지 않고 결과 객체로 통일.
|
|
239
|
+
// try/catch 안에서 emit + return 모두 처리하여 fire-and-forget 호출자도 안전하게 한다.
|
|
240
|
+
const promise = (async () => {
|
|
241
|
+
try {
|
|
242
|
+
await this.pushManager.enable();
|
|
243
|
+
this.emit('pushEnabled');
|
|
244
|
+
return { ok: true };
|
|
245
|
+
} catch (error) {
|
|
246
|
+
// PushError 면 분류 코드 사용, 아니면 'UNKNOWN'.
|
|
247
|
+
const reason = (error && error.code) ? error.code : 'UNKNOWN';
|
|
248
|
+
this.logger.warn('Failed to enable push notifications:', reason, error?.message);
|
|
249
|
+
this.emit('pushFailed', { reason, error });
|
|
250
|
+
return { ok: false, reason, error };
|
|
251
|
+
}
|
|
252
|
+
})();
|
|
253
|
+
|
|
254
|
+
this._pushEnablePromise = promise;
|
|
255
|
+
|
|
256
|
+
// self-clear — 본인이 현재 진행 중인 Promise 일 때만 null 처리 (race 방지).
|
|
257
|
+
promise.finally(() => {
|
|
258
|
+
if (this._pushEnablePromise === promise) {
|
|
259
|
+
this._pushEnablePromise = null;
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
return promise;
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
TalkFlowClient.prototype.consumePendingRoom = async function consumePendingRoom() {
|
|
267
|
+
return PushManager.consumePendingRoom(this.options.projectId);
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
TalkFlowClient.prototype.setCurrentDeviceEnabled = function setCurrentDeviceEnabled(enabled) {
|
|
271
|
+
return this.pushManager?.setCurrentDeviceEnabled(enabled);
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
TalkFlowClient.prototype.setDeviceEnabled = function setDeviceEnabled(deviceId, enabled) {
|
|
275
|
+
return this.pushManager?.setDeviceEnabled(deviceId, enabled);
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
TalkFlowClient.prototype.getMyDevices = function getMyDevices() {
|
|
279
|
+
return this.pushManager?.getMyDevices();
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* 현재 브라우저의 푸시 권한 상태를 조회한다.
|
|
284
|
+
*
|
|
285
|
+
* <p>{@code enablePushNotifications()} 호출 <b>전에도</b> 사용 가능 — pushManager 인스턴스 불필요.
|
|
286
|
+
* UI 분기 (granted → 자동 enable / default → 버튼 노출 / denied → 설정 가이드 / unsupported → 숨김) 에 사용한다.</p>
|
|
287
|
+
*
|
|
288
|
+
* @returns {'granted'|'denied'|'default'|'unsupported'}
|
|
289
|
+
*/
|
|
290
|
+
TalkFlowClient.prototype.getPushPermissionState = function getPushPermissionState() {
|
|
291
|
+
return PushManager.getPermissionState();
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* SDK 내부 푸시 활성화 상태 (FCM 토큰 발급 + 서버 등록까지 완료 여부).
|
|
296
|
+
*
|
|
297
|
+
* <p>주의: 이 값은 SDK 인스턴스의 메모리 상태일 뿐, 브라우저 권한 상태와 다르다.
|
|
298
|
+
* 페이지 새로고침 후에는 enable 호출 전까지 false. 권한 상태는 {@link #getPushPermissionState} 사용.</p>
|
|
299
|
+
*
|
|
300
|
+
* @returns {boolean}
|
|
301
|
+
*/
|
|
302
|
+
TalkFlowClient.prototype.isPushEnabled = function isPushEnabled() {
|
|
303
|
+
return this.pushManager?.isEnabled() ?? false;
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* 현재 발급된 FCM 토큰. enable 호출 후에만 유효, 그 외에는 null.
|
|
308
|
+
* @returns {string|null}
|
|
309
|
+
*/
|
|
310
|
+
TalkFlowClient.prototype.getPushToken = function getPushToken() {
|
|
311
|
+
return this.pushManager?.getToken() ?? null;
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* 푸시 상태 초기화 — 포그라운드 리스너 / FCM 토큰 / SW projectId 등록을 해제한다.
|
|
316
|
+
* 로그아웃 또는 사용자 전환 시 호출. (logout() 내부에서도 자동 호출되므로 일반 흐름에선 불필요.)
|
|
317
|
+
*/
|
|
318
|
+
TalkFlowClient.prototype.resetPush = function resetPush() {
|
|
319
|
+
this.pushManager?.reset();
|
|
320
|
+
this._pushEnablePromise = null;
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
TalkFlowClient.prototype.setToken = async function setToken(token) {
|
|
324
|
+
const { userId } = validateAndParseJWT(token, { validateExpiry: false });
|
|
325
|
+
const needReinitialize = this.userId && this.userId !== userId;
|
|
326
|
+
|
|
327
|
+
this.options.jwtToken = token;
|
|
328
|
+
this._userId = userId;
|
|
329
|
+
this.apiClient.setJwtToken(token);
|
|
330
|
+
|
|
331
|
+
if (needReinitialize && this.connectionManager) {
|
|
332
|
+
await this.disconnect();
|
|
333
|
+
if (this.pushManager) {
|
|
334
|
+
this.pushManager.reset();
|
|
335
|
+
this._pushEnablePromise = null;
|
|
336
|
+
}
|
|
337
|
+
this.connectionManager = null;
|
|
338
|
+
this._chat = null;
|
|
339
|
+
this._webrtc = null;
|
|
340
|
+
|
|
341
|
+
this._initializeSubClients();
|
|
342
|
+
} else if (this.connectionManager) {
|
|
343
|
+
this.connectionManager.updateToken(token);
|
|
344
|
+
} else {
|
|
345
|
+
this._initializeSubClients();
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
this.logger.info('JWT token set', { userId });
|
|
349
|
+
this.emit('tokenSet', { userId });
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
TalkFlowClient.prototype.updateToken = async function updateToken(newToken) {
|
|
353
|
+
await this.setToken(newToken);
|
|
354
|
+
};
|
|
355
|
+
}
|