@vibexnpm/talkx 8.13.0 → 9.2.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/README.md +3 -2
- package/dist/index.d.ts +708 -155
- 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 +4 -2
- package/src/TalkFlowClient.js +13 -5
- package/src/chat/ChatClient.js +159 -2
- package/src/constants.js +52 -41
- package/src/core/ConnectionManager.js +11 -1
- package/src/index.js +5 -4
- package/src/talkflow/delegates.js +19 -23
- package/src/talkflow/eventForwarding.js +11 -23
- package/src/talkflow/session.js +37 -4
- package/src/utils/ApiClient.js +45 -41
- package/src/webrtc/CallLedger.js +135 -0
- package/src/webrtc/LiveKitMediaSession.js +1568 -0
- package/src/webrtc/WebRTCClient.js +909 -1068
- package/types/__typetests__/call.types.ts +101 -0
- package/types/__typetests__/roomOrigin.types.ts +32 -0
- package/types/__typetests__/scheduledReports.types.ts +138 -0
- package/types/index.d.ts +708 -155
- package/src/webrtc/MediaStreamManager.js +0 -478
- package/src/webrtc/PeerConnectionManager.js +0 -476
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibexnpm/talkx",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "9.2.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "TalkFlow SDK - Chat &
|
|
5
|
+
"description": "TalkFlow SDK - Chat & video call (LiveKit SFU) unified client",
|
|
6
6
|
"main": "dist/talkflow-sdk.umd.js",
|
|
7
7
|
"module": "dist/talkflow-sdk.esm.js",
|
|
8
8
|
"browser": "dist/talkflow-sdk.standalone.js",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"talkflow",
|
|
29
29
|
"chat",
|
|
30
30
|
"webrtc",
|
|
31
|
+
"livekit",
|
|
31
32
|
"websocket",
|
|
32
33
|
"stomp",
|
|
33
34
|
"realtime"
|
|
@@ -37,6 +38,7 @@
|
|
|
37
38
|
"dependencies": {
|
|
38
39
|
"@stomp/stompjs": "^7.0.0",
|
|
39
40
|
"firebase": "^11.8.1",
|
|
41
|
+
"livekit-client": "2.22.4",
|
|
40
42
|
"sockjs-client": "^1.6.0"
|
|
41
43
|
},
|
|
42
44
|
"devDependencies": {
|
package/src/TalkFlowClient.js
CHANGED
|
@@ -31,7 +31,7 @@ class TalkFlowClient extends EventEmitter {
|
|
|
31
31
|
* @param {boolean} [options.useSockJS=true] - SockJS 사용 여부
|
|
32
32
|
* @param {number} [options.reconnectDelay=5000] - 재연결 지연 (ms)
|
|
33
33
|
* @param {number} [options.maxReconnectAttempts=10] - 최대 재연결 시도
|
|
34
|
-
* @param {
|
|
34
|
+
* @param {number} [options.callRingTimeoutMs=65000] - 받는 쪽 울림을 스스로 끄는 시각(서버 «응답함»·«취소» 가 먼저 오면 그때 끈다)
|
|
35
35
|
* @param {boolean} [options.autoSubscribeRoomList=true] - connect() 시 채팅방 리스트 자동 구독 여부 (카톡 스타일)
|
|
36
36
|
* @param {number} [options.logLevel=LogLevel.WARN] - 로그 레벨
|
|
37
37
|
*
|
|
@@ -89,7 +89,7 @@ class TalkFlowClient extends EventEmitter {
|
|
|
89
89
|
: DefaultConfig.rewindMillis,
|
|
90
90
|
maxSyncPages: options.maxSyncPages || DefaultConfig.maxSyncPages,
|
|
91
91
|
syncPageSize: options.syncPageSize || DefaultConfig.syncPageSize,
|
|
92
|
-
|
|
92
|
+
callRingTimeoutMs: options.callRingTimeoutMs || DefaultConfig.callRingTimeoutMs,
|
|
93
93
|
// connect() 시 채팅방 리스트 자동 구독 여부 (카톡 스타일).
|
|
94
94
|
// 기본 true — 앱 어디서든 리스트 이벤트를 놓치지 않음 (배지/알림 등).
|
|
95
95
|
// false 로 설정 시 수동으로 client.chat.subscribeRoomList() 호출 필요.
|
|
@@ -241,6 +241,11 @@ class TalkFlowClient extends EventEmitter {
|
|
|
241
241
|
}
|
|
242
242
|
|
|
243
243
|
async _acceptAuthSession(data) {
|
|
244
|
+
// 로그인 세션은 클라이언트에 알리기 전에 새 신원의 토큰으로 바꾼다(AuthSession._accept) — 사용자·회사가 바뀌었으면 옛 신원의 통화
|
|
245
|
+
// 요청(나가기·재시도 등)은 새 신원 이름으로 간다. 이 기기에서만 끝내고 옛 신원의 요청을 멈춘다(서버는 미디어 끊김 뒤 참가를 닫는다).
|
|
246
|
+
if (this.options.projectId !== data.projectId || this.userId !== data.user.userId) {
|
|
247
|
+
await this._webrtc?._resetIdentity();
|
|
248
|
+
}
|
|
244
249
|
if (this.options.projectId && this.options.projectId !== data.projectId && this.connectionManager) {
|
|
245
250
|
await this._clearAuthSession();
|
|
246
251
|
}
|
|
@@ -252,6 +257,10 @@ class TalkFlowClient extends EventEmitter {
|
|
|
252
257
|
}
|
|
253
258
|
|
|
254
259
|
async _clearAuthSession() {
|
|
260
|
+
// 신원이 지워졌거나(로그아웃·다른 탭·자격 만료) 다른 회사로 바뀌었다 — 통화 나가기를 보낼 자격이 없다. 이 기기에서만 끝내고 옛
|
|
261
|
+
// 신원의 요청을 멈춘다(보내면 갱신 토큰 없는 갱신 → 로그아웃 표식이 다른 탭 로그인까지 끊는다). 이 탭의 logout() 은 자격이 살아
|
|
262
|
+
// 있을 때 먼저 나갔다.
|
|
263
|
+
await this._webrtc?._resetIdentity();
|
|
255
264
|
this._authConnectionRequested = false;
|
|
256
265
|
this.options.jwtToken = null;
|
|
257
266
|
this._userId = null;
|
|
@@ -682,9 +691,8 @@ class TalkFlowClient extends EventEmitter {
|
|
|
682
691
|
} : null,
|
|
683
692
|
webrtc: this.webrtc ? {
|
|
684
693
|
isInCall: this.webrtc.isInCall(),
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
mediaState: this.webrtc.getMediaState()
|
|
694
|
+
currentCall: this.webrtc.getCurrentCall(),
|
|
695
|
+
ringingCalls: this.webrtc.getRingingCalls()
|
|
688
696
|
} : null
|
|
689
697
|
};
|
|
690
698
|
}
|
package/src/chat/ChatClient.js
CHANGED
|
@@ -283,11 +283,13 @@ class ChatClient extends EventEmitter {
|
|
|
283
283
|
* 'GROUP' 은 "그룹 탭" 시맨틱 — PRIVATE_GROUP/TEAM 을 포함한 그룹 전체를 반환
|
|
284
284
|
* (PRIVATE_GROUP/TEAM 값을 보내도 동일하게 그룹 전체 — 타입별 정확 필터 아님).
|
|
285
285
|
* 특정 타입만 필요하면 응답의 {@code roomType} 필드로 클라이언트에서 거른다.
|
|
286
|
+
* @param {'USER'|'SCHEDULED_REPORT'} [params.origin] - 만든 경위 필터. {@code USER} = 사람이 만든 방,
|
|
287
|
+
* {@code SCHEDULED_REPORT} = 예약 보고 전용 방. 생략 시 전부. 커서 페이지라 클라이언트에서 거르면 뒤쪽 방을 놓친다.
|
|
286
288
|
* @returns {Promise<Object>}
|
|
287
289
|
*/
|
|
288
290
|
async getRooms(params = {}) {
|
|
289
|
-
const { size = 50, lastId, lastSortValue, type } = params;
|
|
290
|
-
return this.apiClient.get('/api/v1/rooms/my', { size, lastId, lastSortValue, type });
|
|
291
|
+
const { size = 50, lastId, lastSortValue, type, origin } = params;
|
|
292
|
+
return this.apiClient.get('/api/v1/rooms/my', { size, lastId, lastSortValue, type, origin });
|
|
291
293
|
}
|
|
292
294
|
|
|
293
295
|
/**
|
|
@@ -1488,6 +1490,161 @@ class ChatClient extends EventEmitter {
|
|
|
1488
1490
|
return this.apiClient.get(`/api/v1/pm-reports/${encodeURIComponent(reportId)}`);
|
|
1489
1491
|
}
|
|
1490
1492
|
|
|
1493
|
+
// ── 예약 보고 ──────────────────────────────────────
|
|
1494
|
+
//
|
|
1495
|
+
// 정해 둔 시각마다 PM 이 정해 둔 일을 해서 예약의 전용 방에 올린다 — 가이드는 docs/scheduled-reports.md.
|
|
1496
|
+
// 요청한 사람이 만든 사람이고, 남의 예약은 없는 예약과 같은 404 다. 결과는 전용 방의 보통 글이라 방 목록·
|
|
1497
|
+
// 메시지·푸시는 기존 경로 그대로고, 여기에는 예약을 다루는 메서드만 있다.
|
|
1498
|
+
//
|
|
1499
|
+
// 커서 페이지가 없는 응답이라 투표처럼 데이터만 돌려준다. 실패 코드는 {@code error.response.code}.
|
|
1500
|
+
|
|
1501
|
+
/**
|
|
1502
|
+
* 내가 만든 예약 목록 (만든 순).
|
|
1503
|
+
*
|
|
1504
|
+
* @returns {Promise<Array<Object>>} 예약 목록
|
|
1505
|
+
*/
|
|
1506
|
+
async getScheduledReports() {
|
|
1507
|
+
const response = await this.apiClient.get('/api/v1/scheduled-reports');
|
|
1508
|
+
return this._unwrapSuccessResponse(response);
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
/**
|
|
1512
|
+
* 예약 하나와 최근 회차 (예정 늦은 순, 최대 10개).
|
|
1513
|
+
*
|
|
1514
|
+
* @param {string} reportId - 예약 id
|
|
1515
|
+
* @returns {Promise<{report: Object, recentRuns: Array<Object>}>}
|
|
1516
|
+
* @throws {Error} 없거나 남의 예약이면 404 {@code SCHEDULED_REPORT_NOT_FOUND}
|
|
1517
|
+
*/
|
|
1518
|
+
async getScheduledReport(reportId) {
|
|
1519
|
+
if (!reportId) {
|
|
1520
|
+
throw new Error('reportId is required');
|
|
1521
|
+
}
|
|
1522
|
+
const response = await this.apiClient.get(`/api/v1/scheduled-reports/${encodeURIComponent(reportId)}`);
|
|
1523
|
+
return this._unwrapSuccessResponse(response);
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
/**
|
|
1527
|
+
* 지금 새로 만들 수 있는 작업 종류.
|
|
1528
|
+
*
|
|
1529
|
+
* <p>키에 PM 권한이 없거나 일시정지 중인 종류는 빠진다. 빈 배열이면 만들기를 막는다.</p>
|
|
1530
|
+
*
|
|
1531
|
+
* @returns {Promise<Array<'RESEARCH'|'ROOM_DIGEST'>>}
|
|
1532
|
+
*/
|
|
1533
|
+
async getScheduledReportKinds() {
|
|
1534
|
+
const response = await this.apiClient.get('/api/v1/scheduled-reports/kinds');
|
|
1535
|
+
return this._unwrapSuccessResponse(response);
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
/**
|
|
1539
|
+
* 방 정리의 원천 방 고르기 — 내가 지금 참여 중이고 메시지 보관 기간이 없는 방.
|
|
1540
|
+
*
|
|
1541
|
+
* @returns {Promise<Array<{roomId: string, name?: string, roomType?: string}>>}
|
|
1542
|
+
*/
|
|
1543
|
+
async getScheduledReportSourceRooms() {
|
|
1544
|
+
const response = await this.apiClient.get('/api/v1/scheduled-reports/source-rooms');
|
|
1545
|
+
return this._unwrapSuccessResponse(response);
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
/**
|
|
1549
|
+
* 보낼 방 고르기 — 만들기·고치기와 같은 판정으로 거른 방이다.
|
|
1550
|
+
*
|
|
1551
|
+
* <p>만들기 화면은 {@code kind}(방 정리는 {@code sourceRoomId} 도), 고치기 화면은 {@code reportId} 를 넘긴다.
|
|
1552
|
+
* {@code reportId} 를 주면 나머지는 보지 않는다.</p>
|
|
1553
|
+
*
|
|
1554
|
+
* @param {Object} params
|
|
1555
|
+
* @param {'RESEARCH'|'ROOM_DIGEST'} [params.kind] - 만들기 화면의 작업 종류
|
|
1556
|
+
* @param {string} [params.sourceRoomId] - 방 정리일 때 필수 — 방 정리는 원천 방으로만 보낸다
|
|
1557
|
+
* @param {string} [params.reportId] - 고치기 화면의 예약 id
|
|
1558
|
+
* @returns {Promise<Array<{roomId: string, name?: string, roomType?: string}>>}
|
|
1559
|
+
* @throws {Error} 고를 기준이 모자라면 400 {@code SCHEDULED_REPORT_SPEC_INVALID}
|
|
1560
|
+
*/
|
|
1561
|
+
async getScheduledReportDeliveryRooms(params = {}) {
|
|
1562
|
+
const { kind, sourceRoomId, reportId } = params;
|
|
1563
|
+
const response = await this.apiClient.get('/api/v1/scheduled-reports/delivery-rooms', { kind, sourceRoomId, reportId });
|
|
1564
|
+
return this._unwrapSuccessResponse(response);
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
/**
|
|
1568
|
+
* 예약 만들기 — 전용 방을 만들고 예약을 저장한다. 응답 {@code homeRoomId} 가 결과가 올라갈 방이다.
|
|
1569
|
+
*
|
|
1570
|
+
* <p>🔴 <b>{@code idempotencyKey} 는 필수다.</b> SDK 가 대신 만들지 않는다 — 만들면 그 값이 호출자에게 남지 않아,
|
|
1571
|
+
* 응답만 유실된 타임아웃에서 같은 키로 다시 보낼 수 없다(새 키는 예약과 방을 하나 더 만든다). 폼마다 한 번 만들고,
|
|
1572
|
+
* 재시도에는 <b>같은 키</b>를 넘긴다 — 서버가 처음 만든 예약을 돌려준다. 503
|
|
1573
|
+
* {@code SCHEDULED_REPORT_CREATE_RESULT_UNKNOWN}·{@code SCHEDULED_REPORT_RETRY_LATER} 도 같은 키로 다시 보낸다.</p>
|
|
1574
|
+
*
|
|
1575
|
+
* @param {Object} report - 예약 폼
|
|
1576
|
+
* @param {string} report.title - 제목 (앞뒤 공백을 뺀 1~50자). 전용 방 이름으로도 쓴다
|
|
1577
|
+
* @param {Object} report.spec - 작업 칸, {@code kind} 로 가른다. {@code RESEARCH} 는 {@code instruction}(1~2,000자),
|
|
1578
|
+
* {@code ROOM_DIGEST} 는 {@code sourceRoomId}·{@code period}·{@code format}·{@code focus}(선택, 0~500자)
|
|
1579
|
+
* @param {Object} report.cadence - 주기. {@code type}·{@code days}({@code WEEKLY} 만)·{@code time}({@code HH:mm})·
|
|
1580
|
+
* {@code zoneId}(비우면 내 프로필 시간대)
|
|
1581
|
+
* @param {string[]} [report.deliveryRoomIds] - 결과를 복사할 방 (0~3개)
|
|
1582
|
+
* @param {Object} options
|
|
1583
|
+
* @param {string} options.idempotencyKey - <b>필수.</b> 공백 없는 인쇄 가능 ASCII 1~200자. 재시도에는 반드시 같은 값
|
|
1584
|
+
* @returns {Promise<Object>} 만든 예약
|
|
1585
|
+
* @throws {Error} {@code idempotencyKey} 가 없으면 서버를 호출하지 않고 즉시 실패한다
|
|
1586
|
+
* @example
|
|
1587
|
+
* const key = crypto.randomUUID();
|
|
1588
|
+
* const report = await chat.createScheduledReport({
|
|
1589
|
+
* title: '개발팀 주간 정리',
|
|
1590
|
+
* spec: { kind: 'ROOM_DIGEST', sourceRoomId, period: 'LAST_7_DAYS', format: 'MINUTES' },
|
|
1591
|
+
* cadence: { type: 'WEEKLY', days: ['MONDAY'], time: '09:00' }
|
|
1592
|
+
* }, { idempotencyKey: key });
|
|
1593
|
+
*/
|
|
1594
|
+
async createScheduledReport(report, options = {}) {
|
|
1595
|
+
const { idempotencyKey } = options;
|
|
1596
|
+
if (!idempotencyKey) {
|
|
1597
|
+
// SDK 가 만들어 주면 호출자는 그 값을 볼 수 없다 — 타임아웃으로 응답만 유실됐을 때
|
|
1598
|
+
// 같은 키로 재시도할 방법이 사라지고, 다음 호출이 예약과 방을 하나 더 만든다.
|
|
1599
|
+
throw new Error(
|
|
1600
|
+
'createScheduledReport: options.idempotencyKey is required — '
|
|
1601
|
+
+ 'generate it once per form and reuse the same value on retry '
|
|
1602
|
+
+ '(재시도에 같은 키를 넘겨야 예약과 방이 중복 생성되지 않는다).'
|
|
1603
|
+
);
|
|
1604
|
+
}
|
|
1605
|
+
const response = await this.apiClient.post(
|
|
1606
|
+
'/api/v1/scheduled-reports',
|
|
1607
|
+
report,
|
|
1608
|
+
undefined,
|
|
1609
|
+
{ 'X-Idempotency-Key': idempotencyKey }
|
|
1610
|
+
);
|
|
1611
|
+
return this._unwrapSuccessResponse(response);
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
/**
|
|
1615
|
+
* 예약 고치기·멈추기·다시 켜기 — 고칠 칸만 보낸다.
|
|
1616
|
+
*
|
|
1617
|
+
* <p>{@code spec} 은 보내면 칸 전체이고 작업 종류·원천 방은 지금 값과 같아야 한다. {@code cadence} 는 보내면 주기
|
|
1618
|
+
* 전체이고 {@code zoneId} 를 비우면 지금 시간대를 쓴다. {@code deliveryRoomIds} 는 보내면 목록 전체다(빈 배열 =
|
|
1619
|
+
* 보내지 않음). 멈추기는 {@code status: 'PAUSED'}, 다시 켜기는 {@code status: 'ACTIVE'} 다.</p>
|
|
1620
|
+
*
|
|
1621
|
+
* @param {string} reportId - 예약 id
|
|
1622
|
+
* @param {Object} changes - 고칠 칸 ({@code title}·{@code spec}·{@code cadence}·{@code deliveryRoomIds}·{@code status})
|
|
1623
|
+
* @returns {Promise<Object>} 바꾼 예약
|
|
1624
|
+
* @throws {Error} 없거나 남의 예약이면 404 {@code SCHEDULED_REPORT_NOT_FOUND}
|
|
1625
|
+
*/
|
|
1626
|
+
async updateScheduledReport(reportId, changes) {
|
|
1627
|
+
if (!reportId) {
|
|
1628
|
+
throw new Error('reportId is required');
|
|
1629
|
+
}
|
|
1630
|
+
const response = await this.apiClient.patch(`/api/v1/scheduled-reports/${encodeURIComponent(reportId)}`, changes);
|
|
1631
|
+
return this._unwrapSuccessResponse(response);
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
/**
|
|
1635
|
+
* 예약 지우기 — 예약만 지우고 전용 방은 남긴다. 방은 {@link #leaveRoom}·{@link #deleteRoom} 으로 정리한다.
|
|
1636
|
+
*
|
|
1637
|
+
* @param {string} reportId - 예약 id
|
|
1638
|
+
* @returns {Promise<void>}
|
|
1639
|
+
* @throws {Error} 없거나 남의 예약이면 404 {@code SCHEDULED_REPORT_NOT_FOUND}
|
|
1640
|
+
*/
|
|
1641
|
+
async deleteScheduledReport(reportId) {
|
|
1642
|
+
if (!reportId) {
|
|
1643
|
+
throw new Error('reportId is required');
|
|
1644
|
+
}
|
|
1645
|
+
await this.apiClient.delete(`/api/v1/scheduled-reports/${encodeURIComponent(reportId)}`);
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1491
1648
|
/**
|
|
1492
1649
|
* 메시지 목록 조회
|
|
1493
1650
|
* @param {string} roomId - 채팅방 ID
|
package/src/constants.js
CHANGED
|
@@ -40,15 +40,15 @@ export const ErrorTypes = {
|
|
|
40
40
|
CHAT_MESSAGE_FAILED: 'CHAT_MESSAGE_FAILED',
|
|
41
41
|
CHAT_SUBSCRIPTION_FAILED: 'CHAT_SUBSCRIPTION_FAILED',
|
|
42
42
|
|
|
43
|
-
//
|
|
43
|
+
// 통화 errors
|
|
44
44
|
MEDIA_ACCESS_DENIED: 'MEDIA_ACCESS_DENIED',
|
|
45
45
|
SCREEN_SHARE_DENIED: 'SCREEN_SHARE_DENIED',
|
|
46
|
-
|
|
47
|
-
ICE_CONNECTION_FAILED: 'ICE_CONNECTION_FAILED',
|
|
48
|
-
SIGNALING_FAILED: 'SIGNALING_FAILED',
|
|
46
|
+
MEDIA_CONNECTION_FAILED: 'MEDIA_CONNECTION_FAILED',
|
|
49
47
|
DEVICE_SWITCH_FAILED: 'DEVICE_SWITCH_FAILED',
|
|
50
48
|
ENUMERATE_DEVICES_FAILED: 'ENUMERATE_DEVICES_FAILED',
|
|
51
49
|
CALL_ROOM_NOT_FOUND: 'CALL_ROOM_NOT_FOUND',
|
|
50
|
+
CALL_ALREADY_ACTIVE: 'CALL_ALREADY_ACTIVE',
|
|
51
|
+
NO_ACTIVE_CALL: 'NO_ACTIVE_CALL',
|
|
52
52
|
|
|
53
53
|
// General errors
|
|
54
54
|
INVALID_STATE: 'INVALID_STATE',
|
|
@@ -56,36 +56,38 @@ export const ErrorTypes = {
|
|
|
56
56
|
};
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
|
-
*
|
|
59
|
+
* 서버 → 클라이언트 통화 이벤트 종류(서버 WebRTCMessageType 의 값). 클라이언트는 통화 신호를 보내지 않는다 — 통화 제어는 REST,
|
|
60
|
+
* 미디어는 LiveKit 이 맡는다(화상통화 SFU 설계 v0.7 I7).
|
|
60
61
|
*/
|
|
61
|
-
export const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
62
|
+
export const CallEventTypes = {
|
|
63
|
+
/** 통화 초대 — 초대 대상의 모든 기기. */
|
|
64
|
+
INVITATION: 'call_invitation',
|
|
65
|
+
/** 그 사용자가 어느 기기에서 응답했다 — 그 사람의 다른 기기는 울림을 끈다. */
|
|
66
|
+
ANSWERED: 'call_answered',
|
|
67
|
+
/** 거절·무응답 — 방장에게. */
|
|
68
|
+
REJECT: 'call_reject',
|
|
69
|
+
/** 울림을 멈춘다 — 취소·종료·무응답 때 참가 기록 없는 초대 대상에게. */
|
|
70
|
+
CANCEL: 'call_cancel',
|
|
71
|
+
/** 통화 종료 — 통화방 토픽. */
|
|
72
|
+
END: 'call_end'
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/** 통화 종류(서버 CallKind). */
|
|
76
|
+
export const CallKind = {
|
|
77
|
+
DIRECT: 'DIRECT',
|
|
78
|
+
GROUP: 'GROUP'
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 통화 상태(`callStateChanged` 의 state). ringing = 받는 쪽 울림, connecting = 입장·미디어 접속 중, connected = 미디어 연결,
|
|
83
|
+
* reconnecting = LiveKit 재접속 또는 «제거됨» 뒤 다시 입장 중, ended = 끝(사유는 reason).
|
|
84
|
+
*/
|
|
85
|
+
export const CallState = {
|
|
86
|
+
RINGING: 'ringing',
|
|
87
|
+
CONNECTING: 'connecting',
|
|
88
|
+
CONNECTED: 'connected',
|
|
89
|
+
RECONNECTING: 'reconnecting',
|
|
90
|
+
ENDED: 'ended'
|
|
89
91
|
};
|
|
90
92
|
|
|
91
93
|
/**
|
|
@@ -216,6 +218,19 @@ export const ChatRoomType = {
|
|
|
216
218
|
TEAM: 'TEAM'
|
|
217
219
|
};
|
|
218
220
|
|
|
221
|
+
/**
|
|
222
|
+
* 방이 생긴 경위 (서버 {@code ChatRoomOrigin} enum 과 일치).
|
|
223
|
+
*
|
|
224
|
+
* <ul>
|
|
225
|
+
* <li>{@code USER} — 사람이 만든 방. 목록 거르기 값이고, 방 응답에는 칸이 오지 않는다</li>
|
|
226
|
+
* <li>{@code SCHEDULED_REPORT} — 예약 보고가 만든 전용 방</li>
|
|
227
|
+
* </ul>
|
|
228
|
+
*/
|
|
229
|
+
export const ChatRoomOrigin = {
|
|
230
|
+
USER: 'USER',
|
|
231
|
+
SCHEDULED_REPORT: 'SCHEDULED_REPORT'
|
|
232
|
+
};
|
|
233
|
+
|
|
219
234
|
/**
|
|
220
235
|
* 어시스턴트 참여 모드 (서버 {@code AssistantMode} enum 과 일치).
|
|
221
236
|
*
|
|
@@ -397,15 +412,14 @@ export const WebSocketPaths = {
|
|
|
397
412
|
// 서버가 convertAndSendToUser(userId, "/queue/mail", MAIL_CHANGED) 로 전송 — 새 메일이면 messages[] 미리보기 포함
|
|
398
413
|
MAIL_USER_DESTINATION: '/user/queue/mail',
|
|
399
414
|
|
|
400
|
-
//
|
|
415
|
+
// 통화 이벤트 — 서버만 보낸다(통화방 토픽은 종료, 개인 큐는 초대·응답·거절·취소)
|
|
401
416
|
getWebRTCDestination: (roomId) => `/topic/webrtc/${roomId}`,
|
|
402
417
|
getWebRTCUserDestination: () => `/user/queue/webrtc`,
|
|
403
418
|
|
|
404
419
|
// Send destinations
|
|
405
420
|
CHAT_SEND: '/app/chat/send',
|
|
406
421
|
CHAT_READ: '/app/chat/read',
|
|
407
|
-
CHAT_TYPING: '/app/chat/typing'
|
|
408
|
-
WEBRTC_SIGNAL: '/app/webrtc/signal'
|
|
422
|
+
CHAT_TYPING: '/app/chat/typing'
|
|
409
423
|
};
|
|
410
424
|
|
|
411
425
|
/**
|
|
@@ -465,11 +479,8 @@ export const DefaultConfig = {
|
|
|
465
479
|
// API
|
|
466
480
|
apiTimeout: 30000,
|
|
467
481
|
|
|
468
|
-
//
|
|
469
|
-
|
|
470
|
-
{ urls: 'stun:stun.l.google.com:19302' },
|
|
471
|
-
{ urls: 'stun:stun1.l.google.com:19302' }
|
|
472
|
-
],
|
|
482
|
+
// 통화 — 받는 쪽 울림을 끄는 시각(서버 울림 상한 60초 + 여유). 서버 «응답함»·«취소» 가 먼저 오면 그때 끈다.
|
|
483
|
+
callRingTimeoutMs: 65000,
|
|
473
484
|
|
|
474
485
|
// Logging
|
|
475
486
|
logLevel: 2 // WARN
|
|
@@ -781,9 +781,19 @@ class ConnectionManager extends EventEmitter {
|
|
|
781
781
|
/**
|
|
782
782
|
* 구독 해제
|
|
783
783
|
* @param {string} destination - 구독 경로
|
|
784
|
+
* @param {Object} [subscription] - 이 구독만 푼다(subscribe 가 돌려준 객체). 끊긴 사이 같은 목적지 구독이 대기 줄에 여럿 섰다가 함께
|
|
785
|
+
* 붙으면 맵에는 마지막 것만 남는다 — 맵 항목은 그것이 이 구독일 때만 지운다. 생략하면 그 목적지의 지금(맵의) 구독을 푼다.
|
|
784
786
|
*/
|
|
785
|
-
unsubscribe(destination) {
|
|
787
|
+
unsubscribe(destination, subscription) {
|
|
786
788
|
const entry = this.subscriptions.get(destination);
|
|
789
|
+
if (subscription) {
|
|
790
|
+
subscription.unsubscribe();
|
|
791
|
+
if (entry?.subscription === subscription) {
|
|
792
|
+
this.subscriptions.delete(destination);
|
|
793
|
+
}
|
|
794
|
+
this.logger.debug(`Unsubscribed from: ${destination}`);
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
787
797
|
if (entry) {
|
|
788
798
|
entry.subscription.unsubscribe();
|
|
789
799
|
this.subscriptions.delete(destination);
|
package/src/index.js
CHANGED
|
@@ -41,10 +41,13 @@ import TalkFlowClient from './TalkFlowClient.js';
|
|
|
41
41
|
export {
|
|
42
42
|
ConnectionState,
|
|
43
43
|
ErrorTypes,
|
|
44
|
-
|
|
44
|
+
CallEventTypes,
|
|
45
|
+
CallKind,
|
|
46
|
+
CallState,
|
|
45
47
|
ChatMessageType,
|
|
46
48
|
DecisionCardLimits,
|
|
47
49
|
ChatRoomType,
|
|
50
|
+
ChatRoomOrigin,
|
|
48
51
|
AssistantMode,
|
|
49
52
|
RoomAiType,
|
|
50
53
|
EngagementIntensity,
|
|
@@ -81,10 +84,8 @@ export { default as ChatClient } from './chat/ChatClient.js';
|
|
|
81
84
|
// 방 미독 계산기 — ROOM_UNREAD_UPDATED / MESSAGE_RECEIVED 를 순서 역전에 안전하게 합산(READ_SYNC_DESIGN §6).
|
|
82
85
|
export { RoomUnreadReconciler } from './chat/RoomUnreadReconciler.js';
|
|
83
86
|
|
|
84
|
-
//
|
|
87
|
+
// 화상통화 (LiveKit SFU)
|
|
85
88
|
export { default as WebRTCClient } from './webrtc/WebRTCClient.js';
|
|
86
|
-
export { default as MediaStreamManager } from './webrtc/MediaStreamManager.js';
|
|
87
|
-
export { default as PeerConnectionManager } from './webrtc/PeerConnectionManager.js';
|
|
88
89
|
|
|
89
90
|
// Push — PushError/PushErrorCode 는 호출자가 instanceof / code 분기에 사용 가능하도록 noamed export.
|
|
90
91
|
export { default as PushManager, PushError, PushErrorCode } from './push/PushManager.js';
|
|
@@ -25,6 +25,15 @@ const CHAT_DELEGATE_METHODS = [
|
|
|
25
25
|
// PM 보고서 — 참여 중인 방들에 PM 이 올린 작업 결과(지금은 자료조사) 목록·상세.
|
|
26
26
|
'getPmReports',
|
|
27
27
|
'getPmReport',
|
|
28
|
+
// 예약 보고 — 정해 둔 시각마다 PM 이 전용 방에 올리는 보고의 만들기·고치기·지우기와 고르기 목록.
|
|
29
|
+
'getScheduledReports',
|
|
30
|
+
'getScheduledReport',
|
|
31
|
+
'getScheduledReportKinds',
|
|
32
|
+
'getScheduledReportSourceRooms',
|
|
33
|
+
'getScheduledReportDeliveryRooms',
|
|
34
|
+
'createScheduledReport',
|
|
35
|
+
'updateScheduledReport',
|
|
36
|
+
'deleteScheduledReport',
|
|
28
37
|
'fetchLinkPreview',
|
|
29
38
|
'sendMessage',
|
|
30
39
|
'sendMessageOptimistic',
|
|
@@ -99,41 +108,28 @@ const CHAT_DELEGATE_METHODS = [
|
|
|
99
108
|
];
|
|
100
109
|
|
|
101
110
|
const WEBRTC_DELEGATE_METHODS = [
|
|
102
|
-
'initializeIceServers',
|
|
103
|
-
'getTurnCredentials',
|
|
104
|
-
'createCallRoom',
|
|
105
|
-
'getCallRoom',
|
|
106
|
-
'joinCallRoomApi',
|
|
107
|
-
'leaveCallRoomApi',
|
|
108
111
|
'enableIncomingCalls',
|
|
109
112
|
'disableIncomingCalls',
|
|
110
113
|
'isIncomingCallsEnabled',
|
|
111
|
-
'startCall',
|
|
112
114
|
'callUser',
|
|
115
|
+
'createGroupCall',
|
|
113
116
|
'acceptCall',
|
|
117
|
+
'joinCall',
|
|
114
118
|
'rejectCall',
|
|
115
119
|
'cancelCall',
|
|
116
|
-
'
|
|
117
|
-
'
|
|
118
|
-
'
|
|
119
|
-
'
|
|
120
|
-
'setAudioEnabled',
|
|
120
|
+
'leaveCall',
|
|
121
|
+
'endCallForAll',
|
|
122
|
+
'setCameraEnabled',
|
|
123
|
+
'setMicrophoneEnabled',
|
|
121
124
|
'startScreenShare',
|
|
122
125
|
'stopScreenShare',
|
|
123
|
-
'getLocalStream',
|
|
124
126
|
'getDevices',
|
|
125
127
|
'switchDevice',
|
|
126
|
-
'
|
|
127
|
-
'
|
|
128
|
-
'applyVideoConstraints',
|
|
129
|
-
'getVideoSettings',
|
|
130
|
-
'getAudioSettings',
|
|
128
|
+
'getCallRoom',
|
|
129
|
+
'getParticipation',
|
|
131
130
|
'isInCall',
|
|
132
|
-
'
|
|
133
|
-
'
|
|
134
|
-
'getMediaState',
|
|
135
|
-
'getConnectionSummary',
|
|
136
|
-
'getStats'
|
|
131
|
+
'getCurrentCall',
|
|
132
|
+
'getRingingCalls'
|
|
137
133
|
];
|
|
138
134
|
|
|
139
135
|
function defineDelegates(prototype, targetKey, methods) {
|
|
@@ -63,32 +63,20 @@ const CHAT_EVENT_MAP = [
|
|
|
63
63
|
];
|
|
64
64
|
|
|
65
65
|
const WEBRTC_EVENT_MAP = [
|
|
66
|
-
['
|
|
67
|
-
['
|
|
68
|
-
['
|
|
69
|
-
['screenShareStarted', 'screenShareStarted'],
|
|
70
|
-
['screenShareEnded', 'screenShareEnded'],
|
|
71
|
-
['deviceChange', 'deviceChange'],
|
|
72
|
-
['mediaStateChanged', 'mediaStateChanged'],
|
|
73
|
-
['callStarted', 'callStarted'],
|
|
66
|
+
['incomingCall', 'incomingCall'],
|
|
67
|
+
['ringStopped', 'ringStopped'],
|
|
68
|
+
['callStateChanged', 'callStateChanged'],
|
|
74
69
|
['callEnded', 'callEnded'],
|
|
75
|
-
['callRecoveryFailed', 'callRecoveryFailed'],
|
|
76
|
-
['callRequested', 'callRequested'],
|
|
77
|
-
['callAccepted', 'callAccepted'],
|
|
78
70
|
['callRejected', 'callRejected'],
|
|
79
|
-
['
|
|
80
|
-
['callBusy', 'callBusy'],
|
|
81
|
-
['incomingCall', 'incomingCall'],
|
|
82
|
-
['incomingCallWhileBusy', 'incomingCallWhileBusy'],
|
|
83
|
-
['callInvitation', 'callInvitation'],
|
|
84
|
-
['userJoined', 'userJoined'],
|
|
85
|
-
['userLeft', 'userLeft'],
|
|
71
|
+
['participantJoined', 'participantJoined'],
|
|
86
72
|
['participantLeft', 'participantLeft'],
|
|
87
|
-
['
|
|
88
|
-
['
|
|
89
|
-
['
|
|
90
|
-
['
|
|
91
|
-
['
|
|
73
|
+
['trackSubscribed', 'trackSubscribed'],
|
|
74
|
+
['trackUnsubscribed', 'trackUnsubscribed'],
|
|
75
|
+
['localTrackPublished', 'localTrackPublished'],
|
|
76
|
+
['localTrackUnpublished', 'localTrackUnpublished'],
|
|
77
|
+
['participantMediaChanged', 'participantMediaChanged'],
|
|
78
|
+
['callRecoveryFailed', 'callRecoveryFailed'],
|
|
79
|
+
['callError', 'callError']
|
|
92
80
|
];
|
|
93
81
|
|
|
94
82
|
function forwardMappedEvents(source, target, eventMap) {
|
package/src/talkflow/session.js
CHANGED
|
@@ -5,6 +5,23 @@ import WebRTCClient from '../webrtc/WebRTCClient.js';
|
|
|
5
5
|
import PushManager from '../push/PushManager.js';
|
|
6
6
|
import { handleMailPush } from './mailNotifications.js';
|
|
7
7
|
|
|
8
|
+
/** 로그아웃·사용자 전환·연결 끊기 전에 통화에서 나가기를 기다리는 상한 — 망이 매달려도 오래 막지 않는다(미디어는 바로 끊긴다). */
|
|
9
|
+
export const LEAVE_CALL_ON_EXIT_TIMEOUT_MS = 3000;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 통화 중이면 나간다 — 자격(토큰·로그인)이 바뀌기 «전» 에 보내야 서버에 닿는다(바꾼 뒤면 다른 사용자 이름으로 가거나 로그아웃 표식
|
|
13
|
+
* 때문에 보내지 못한다). 실패·지연은 기다리지 않는다 — 서버는 끊김 유예 뒤 닫는다.
|
|
14
|
+
*/
|
|
15
|
+
async function leaveCallBeforeExit(client) {
|
|
16
|
+
if (!client.webrtc?.isInCall()) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
let timer;
|
|
20
|
+
const leave = client.webrtc.leaveCall().catch(error => client.logger.warn('Failed to leave the call before exit:', error));
|
|
21
|
+
await Promise.race([leave, new Promise((resolve) => { timer = setTimeout(resolve, LEAVE_CALL_ON_EXIT_TIMEOUT_MS); })]);
|
|
22
|
+
clearTimeout(timer);
|
|
23
|
+
}
|
|
24
|
+
|
|
8
25
|
export function initializeSubClients(client) {
|
|
9
26
|
if (client.connectionManager) {
|
|
10
27
|
return;
|
|
@@ -52,7 +69,7 @@ export function initializeSubClients(client) {
|
|
|
52
69
|
connectionManager: client.connectionManager,
|
|
53
70
|
apiClient: client.apiClient,
|
|
54
71
|
userId: client._userId,
|
|
55
|
-
|
|
72
|
+
ringTimeoutMs: client.options.callRingTimeoutMs,
|
|
56
73
|
logLevel: client.options.logLevel
|
|
57
74
|
});
|
|
58
75
|
|
|
@@ -120,9 +137,7 @@ export function applySessionMethods(TalkFlowClient) {
|
|
|
120
137
|
return;
|
|
121
138
|
}
|
|
122
139
|
|
|
123
|
-
|
|
124
|
-
this.webrtc.endCall();
|
|
125
|
-
}
|
|
140
|
+
await leaveCallBeforeExit(this);
|
|
126
141
|
|
|
127
142
|
if (this.webrtc) {
|
|
128
143
|
this.webrtc.disableIncomingCalls();
|
|
@@ -139,6 +154,10 @@ export function applySessionMethods(TalkFlowClient) {
|
|
|
139
154
|
};
|
|
140
155
|
|
|
141
156
|
TalkFlowClient.prototype.logout = async function logout() {
|
|
157
|
+
await leaveCallBeforeExit(this);
|
|
158
|
+
// 이 신원의 통화는 여기서 끝 — 늦게 끝나는 요청(만드는 중이던 통화의 취소·재시도 등)이 자격이 바뀐 뒤에 나가지 않게 한다. 토큰
|
|
159
|
+
// 직접 방식은 WebRTCClient 를 다음 setToken 에 그대로 쓴다.
|
|
160
|
+
await this._webrtc?._resetIdentity();
|
|
142
161
|
if (this.authSession) return this.authSession.logout();
|
|
143
162
|
try {
|
|
144
163
|
await this.apiClient.post('/v1/auth/signout');
|
|
@@ -362,6 +381,15 @@ export function applySessionMethods(TalkFlowClient) {
|
|
|
362
381
|
const { userId } = validateAndParseJWT(token, { validateExpiry: false });
|
|
363
382
|
const needReinitialize = this.userId && this.userId !== userId;
|
|
364
383
|
|
|
384
|
+
if (needReinitialize && this.connectionManager) {
|
|
385
|
+
// 다른 사용자로 바꾸기 전에 지금 사용자의 통화에서 나간다 — 토큰을 바꾼 뒤면 나가기가 새 사용자 이름으로 간다. 나간 뒤 화상통화를
|
|
386
|
+
// 바로 닫아 늦게 끝나는 요청(만드는 중이던 통화의 취소 등)도 새 사용자 이름으로 나가지 않게 한다. 로그인 세션 방식은 토큰이
|
|
387
|
+
// 이미 바뀌어 있어 _acceptAuthSession 이 먼저 이 기기에서만 끝냈다.
|
|
388
|
+
await leaveCallBeforeExit(this);
|
|
389
|
+
await this._webrtc?._resetIdentity();
|
|
390
|
+
this._webrtc?.destroy();
|
|
391
|
+
}
|
|
392
|
+
|
|
365
393
|
this.options.jwtToken = token;
|
|
366
394
|
this._userId = userId;
|
|
367
395
|
this.apiClient.setJwtToken(token);
|
|
@@ -386,6 +414,11 @@ export function applySessionMethods(TalkFlowClient) {
|
|
|
386
414
|
this._initializeSubClients();
|
|
387
415
|
} else if (this.connectionManager) {
|
|
388
416
|
this.connectionManager.updateToken(token);
|
|
417
|
+
// 토큰 직접 방식의 로그아웃 뒤 다른 사용자로 setToken 하면 재초기화 없이 여기로 온다 — «다른 기기에서 받음» 판정이 새 사용자로
|
|
418
|
+
// 하게 한다(옛 신원은 logout 이 _resetIdentity 로 끝냈다).
|
|
419
|
+
if (this._webrtc) {
|
|
420
|
+
this._webrtc.userId = userId;
|
|
421
|
+
}
|
|
389
422
|
} else {
|
|
390
423
|
this._initializeSubClients();
|
|
391
424
|
}
|