@vibexnpm/talkx 8.12.0 → 9.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/README.md +3 -2
- package/dist/index.d.ts +775 -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 +192 -0
- package/src/constants.js +39 -41
- package/src/core/ConnectionManager.js +11 -1
- package/src/index.js +4 -4
- package/src/talkflow/delegates.js +22 -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__/pmReports.types.ts +55 -0
- package/types/__typetests__/scheduledReports.types.ts +138 -0
- package/types/index.d.ts +775 -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.1.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
|
@@ -1451,6 +1451,198 @@ class ChatClient extends EventEmitter {
|
|
|
1451
1451
|
return this.apiClient.get('/api/v1/files', { roomId, type, size, lastId, lastSortValue });
|
|
1452
1452
|
}
|
|
1453
1453
|
|
|
1454
|
+
/**
|
|
1455
|
+
* PM 보고서 목록 — 내가 지금 참여 중인 방들에 PM 이 올린 작업 결과(지금은 자료조사)를 최신순 커서 페이지로.
|
|
1456
|
+
*
|
|
1457
|
+
* <p>한 줄이 결과 하나다 — 제목(위임 요지)·미리보기(결과 앞 200자)·방·시킨 사람·PM·올라온 시각. 본문은
|
|
1458
|
+
* {@link ChatClient#getPmReport} 로 연다. 보이는 범위는 파일함과 같다 — 참여 중인 방만, 재입장 이전·보관 기간 밖·
|
|
1459
|
+
* 나만 삭제한 결과는 오지 않는다. 실패 안내·단순 답변은 보고서가 아니다.</p>
|
|
1460
|
+
*
|
|
1461
|
+
* <p>{@link ChatClient#getFiles} 처럼 서버 응답 래퍼째 돌려준다 — 페이지는 {@code result.data}. 끝은 {@code hasNext}
|
|
1462
|
+
* 로만 판단한다 — 드물게 짧거나 빈 페이지가 {@code hasNext: true} 와 함께 온다(게시 뒤 결과가 지워진 줄은 빠진다).</p>
|
|
1463
|
+
*
|
|
1464
|
+
* @param {Object} [params]
|
|
1465
|
+
* @param {'RESEARCH'} [params.kind] - 보고서 종류 — 지금은 {@code RESEARCH}(자료조사) 하나, 생략하면 자료조사.
|
|
1466
|
+
* 그 밖의 값이면 400. 종류·방을 바꾸면 커서 없이 처음부터 부른다
|
|
1467
|
+
* @param {string} [params.roomId] - 방 하나로 거르기. 생략하면 참여 중인 방 전부. 참여하지 않은 방이면 403
|
|
1468
|
+
* @param {number} [params.size=50] - 페이지 크기 (1-100)
|
|
1469
|
+
* @param {string} [params.lastId] - 직전 응답의 lastId 를 그대로
|
|
1470
|
+
* @param {number} [params.lastSortValue] - 직전 응답의 lastSortValue 를 그대로
|
|
1471
|
+
* @returns {Promise<Object>}
|
|
1472
|
+
*/
|
|
1473
|
+
async getPmReports(params = {}) {
|
|
1474
|
+
const { kind, roomId, size = 50, lastId, lastSortValue } = params;
|
|
1475
|
+
return this.apiClient.get('/api/v1/pm-reports', { kind, roomId, size, lastId, lastSortValue });
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
/**
|
|
1479
|
+
* PM 보고서 상세 — 목록 한 줄과 같은 칸 + 본문({@code content}, 마크다운 · 출처 절 포함).
|
|
1480
|
+
*
|
|
1481
|
+
* <p>채팅에서 결과에 붙었던 안내도 같은 문구로 붙는다 — 대화 변경 고지(본문 위)·이미지 분석 범위 안내(본문 끝).
|
|
1482
|
+
* 없거나 볼 수 없으면(나만 삭제·방에서 나감 등) 404 하나로 답한다. 반환은 래퍼째({@code result.data}).</p>
|
|
1483
|
+
*
|
|
1484
|
+
* @param {string} reportId - 목록 한 줄의 {@code id}
|
|
1485
|
+
* @returns {Promise<Object>}
|
|
1486
|
+
*/
|
|
1487
|
+
async getPmReport(reportId) {
|
|
1488
|
+
return this.apiClient.get(`/api/v1/pm-reports/${encodeURIComponent(reportId)}`);
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
// ── 예약 보고 ──────────────────────────────────────
|
|
1492
|
+
//
|
|
1493
|
+
// 정해 둔 시각마다 PM 이 정해 둔 일을 해서 예약의 전용 방에 올린다 — 가이드는 docs/scheduled-reports.md.
|
|
1494
|
+
// 요청한 사람이 만든 사람이고, 남의 예약은 없는 예약과 같은 404 다. 결과는 전용 방의 보통 글이라 방 목록·
|
|
1495
|
+
// 메시지·푸시는 기존 경로 그대로고, 여기에는 예약을 다루는 메서드만 있다.
|
|
1496
|
+
//
|
|
1497
|
+
// 커서 페이지가 없는 응답이라 투표처럼 데이터만 돌려준다. 실패 코드는 {@code error.response.code}.
|
|
1498
|
+
|
|
1499
|
+
/**
|
|
1500
|
+
* 내가 만든 예약 목록 (만든 순).
|
|
1501
|
+
*
|
|
1502
|
+
* @returns {Promise<Array<Object>>} 예약 목록
|
|
1503
|
+
*/
|
|
1504
|
+
async getScheduledReports() {
|
|
1505
|
+
const response = await this.apiClient.get('/api/v1/scheduled-reports');
|
|
1506
|
+
return this._unwrapSuccessResponse(response);
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
/**
|
|
1510
|
+
* 예약 하나와 최근 회차 (예정 늦은 순, 최대 10개).
|
|
1511
|
+
*
|
|
1512
|
+
* @param {string} reportId - 예약 id
|
|
1513
|
+
* @returns {Promise<{report: Object, recentRuns: Array<Object>}>}
|
|
1514
|
+
* @throws {Error} 없거나 남의 예약이면 404 {@code SCHEDULED_REPORT_NOT_FOUND}
|
|
1515
|
+
*/
|
|
1516
|
+
async getScheduledReport(reportId) {
|
|
1517
|
+
if (!reportId) {
|
|
1518
|
+
throw new Error('reportId is required');
|
|
1519
|
+
}
|
|
1520
|
+
const response = await this.apiClient.get(`/api/v1/scheduled-reports/${encodeURIComponent(reportId)}`);
|
|
1521
|
+
return this._unwrapSuccessResponse(response);
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
/**
|
|
1525
|
+
* 지금 새로 만들 수 있는 작업 종류.
|
|
1526
|
+
*
|
|
1527
|
+
* <p>키에 PM 권한이 없거나 일시정지 중인 종류는 빠진다. 빈 배열이면 만들기를 막는다.</p>
|
|
1528
|
+
*
|
|
1529
|
+
* @returns {Promise<Array<'RESEARCH'|'ROOM_DIGEST'>>}
|
|
1530
|
+
*/
|
|
1531
|
+
async getScheduledReportKinds() {
|
|
1532
|
+
const response = await this.apiClient.get('/api/v1/scheduled-reports/kinds');
|
|
1533
|
+
return this._unwrapSuccessResponse(response);
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
/**
|
|
1537
|
+
* 방 정리의 원천 방 고르기 — 내가 지금 참여 중이고 메시지 보관 기간이 없는 방.
|
|
1538
|
+
*
|
|
1539
|
+
* @returns {Promise<Array<{roomId: string, name?: string, roomType?: string}>>}
|
|
1540
|
+
*/
|
|
1541
|
+
async getScheduledReportSourceRooms() {
|
|
1542
|
+
const response = await this.apiClient.get('/api/v1/scheduled-reports/source-rooms');
|
|
1543
|
+
return this._unwrapSuccessResponse(response);
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
/**
|
|
1547
|
+
* 보낼 방 고르기 — 만들기·고치기와 같은 판정으로 거른 방이다.
|
|
1548
|
+
*
|
|
1549
|
+
* <p>만들기 화면은 {@code kind}(방 정리는 {@code sourceRoomId} 도), 고치기 화면은 {@code reportId} 를 넘긴다.
|
|
1550
|
+
* {@code reportId} 를 주면 나머지는 보지 않는다.</p>
|
|
1551
|
+
*
|
|
1552
|
+
* @param {Object} params
|
|
1553
|
+
* @param {'RESEARCH'|'ROOM_DIGEST'} [params.kind] - 만들기 화면의 작업 종류
|
|
1554
|
+
* @param {string} [params.sourceRoomId] - 방 정리일 때 필수 — 방 정리는 원천 방으로만 보낸다
|
|
1555
|
+
* @param {string} [params.reportId] - 고치기 화면의 예약 id
|
|
1556
|
+
* @returns {Promise<Array<{roomId: string, name?: string, roomType?: string}>>}
|
|
1557
|
+
* @throws {Error} 고를 기준이 모자라면 400 {@code SCHEDULED_REPORT_SPEC_INVALID}
|
|
1558
|
+
*/
|
|
1559
|
+
async getScheduledReportDeliveryRooms(params = {}) {
|
|
1560
|
+
const { kind, sourceRoomId, reportId } = params;
|
|
1561
|
+
const response = await this.apiClient.get('/api/v1/scheduled-reports/delivery-rooms', { kind, sourceRoomId, reportId });
|
|
1562
|
+
return this._unwrapSuccessResponse(response);
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
/**
|
|
1566
|
+
* 예약 만들기 — 전용 방을 만들고 예약을 저장한다. 응답 {@code homeRoomId} 가 결과가 올라갈 방이다.
|
|
1567
|
+
*
|
|
1568
|
+
* <p>🔴 <b>{@code idempotencyKey} 는 필수다.</b> SDK 가 대신 만들지 않는다 — 만들면 그 값이 호출자에게 남지 않아,
|
|
1569
|
+
* 응답만 유실된 타임아웃에서 같은 키로 다시 보낼 수 없다(새 키는 예약과 방을 하나 더 만든다). 폼마다 한 번 만들고,
|
|
1570
|
+
* 재시도에는 <b>같은 키</b>를 넘긴다 — 서버가 처음 만든 예약을 돌려준다. 503
|
|
1571
|
+
* {@code SCHEDULED_REPORT_CREATE_RESULT_UNKNOWN}·{@code SCHEDULED_REPORT_RETRY_LATER} 도 같은 키로 다시 보낸다.</p>
|
|
1572
|
+
*
|
|
1573
|
+
* @param {Object} report - 예약 폼
|
|
1574
|
+
* @param {string} report.title - 제목 (앞뒤 공백을 뺀 1~50자). 전용 방 이름으로도 쓴다
|
|
1575
|
+
* @param {Object} report.spec - 작업 칸, {@code kind} 로 가른다. {@code RESEARCH} 는 {@code instruction}(1~2,000자),
|
|
1576
|
+
* {@code ROOM_DIGEST} 는 {@code sourceRoomId}·{@code period}·{@code format}·{@code focus}(선택, 0~500자)
|
|
1577
|
+
* @param {Object} report.cadence - 주기. {@code type}·{@code days}({@code WEEKLY} 만)·{@code time}({@code HH:mm})·
|
|
1578
|
+
* {@code zoneId}(비우면 내 프로필 시간대)
|
|
1579
|
+
* @param {string[]} [report.deliveryRoomIds] - 결과를 복사할 방 (0~3개)
|
|
1580
|
+
* @param {Object} options
|
|
1581
|
+
* @param {string} options.idempotencyKey - <b>필수.</b> 공백 없는 인쇄 가능 ASCII 1~200자. 재시도에는 반드시 같은 값
|
|
1582
|
+
* @returns {Promise<Object>} 만든 예약
|
|
1583
|
+
* @throws {Error} {@code idempotencyKey} 가 없으면 서버를 호출하지 않고 즉시 실패한다
|
|
1584
|
+
* @example
|
|
1585
|
+
* const key = crypto.randomUUID();
|
|
1586
|
+
* const report = await chat.createScheduledReport({
|
|
1587
|
+
* title: '개발팀 주간 정리',
|
|
1588
|
+
* spec: { kind: 'ROOM_DIGEST', sourceRoomId, period: 'LAST_7_DAYS', format: 'MINUTES' },
|
|
1589
|
+
* cadence: { type: 'WEEKLY', days: ['MONDAY'], time: '09:00' }
|
|
1590
|
+
* }, { idempotencyKey: key });
|
|
1591
|
+
*/
|
|
1592
|
+
async createScheduledReport(report, options = {}) {
|
|
1593
|
+
const { idempotencyKey } = options;
|
|
1594
|
+
if (!idempotencyKey) {
|
|
1595
|
+
// SDK 가 만들어 주면 호출자는 그 값을 볼 수 없다 — 타임아웃으로 응답만 유실됐을 때
|
|
1596
|
+
// 같은 키로 재시도할 방법이 사라지고, 다음 호출이 예약과 방을 하나 더 만든다.
|
|
1597
|
+
throw new Error(
|
|
1598
|
+
'createScheduledReport: options.idempotencyKey is required — '
|
|
1599
|
+
+ 'generate it once per form and reuse the same value on retry '
|
|
1600
|
+
+ '(재시도에 같은 키를 넘겨야 예약과 방이 중복 생성되지 않는다).'
|
|
1601
|
+
);
|
|
1602
|
+
}
|
|
1603
|
+
const response = await this.apiClient.post(
|
|
1604
|
+
'/api/v1/scheduled-reports',
|
|
1605
|
+
report,
|
|
1606
|
+
undefined,
|
|
1607
|
+
{ 'X-Idempotency-Key': idempotencyKey }
|
|
1608
|
+
);
|
|
1609
|
+
return this._unwrapSuccessResponse(response);
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
/**
|
|
1613
|
+
* 예약 고치기·멈추기·다시 켜기 — 고칠 칸만 보낸다.
|
|
1614
|
+
*
|
|
1615
|
+
* <p>{@code spec} 은 보내면 칸 전체이고 작업 종류·원천 방은 지금 값과 같아야 한다. {@code cadence} 는 보내면 주기
|
|
1616
|
+
* 전체이고 {@code zoneId} 를 비우면 지금 시간대를 쓴다. {@code deliveryRoomIds} 는 보내면 목록 전체다(빈 배열 =
|
|
1617
|
+
* 보내지 않음). 멈추기는 {@code status: 'PAUSED'}, 다시 켜기는 {@code status: 'ACTIVE'} 다.</p>
|
|
1618
|
+
*
|
|
1619
|
+
* @param {string} reportId - 예약 id
|
|
1620
|
+
* @param {Object} changes - 고칠 칸 ({@code title}·{@code spec}·{@code cadence}·{@code deliveryRoomIds}·{@code status})
|
|
1621
|
+
* @returns {Promise<Object>} 바꾼 예약
|
|
1622
|
+
* @throws {Error} 없거나 남의 예약이면 404 {@code SCHEDULED_REPORT_NOT_FOUND}
|
|
1623
|
+
*/
|
|
1624
|
+
async updateScheduledReport(reportId, changes) {
|
|
1625
|
+
if (!reportId) {
|
|
1626
|
+
throw new Error('reportId is required');
|
|
1627
|
+
}
|
|
1628
|
+
const response = await this.apiClient.patch(`/api/v1/scheduled-reports/${encodeURIComponent(reportId)}`, changes);
|
|
1629
|
+
return this._unwrapSuccessResponse(response);
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
/**
|
|
1633
|
+
* 예약 지우기 — 예약만 지우고 전용 방은 남긴다. 방은 {@link #leaveRoom}·{@link #deleteRoom} 으로 정리한다.
|
|
1634
|
+
*
|
|
1635
|
+
* @param {string} reportId - 예약 id
|
|
1636
|
+
* @returns {Promise<void>}
|
|
1637
|
+
* @throws {Error} 없거나 남의 예약이면 404 {@code SCHEDULED_REPORT_NOT_FOUND}
|
|
1638
|
+
*/
|
|
1639
|
+
async deleteScheduledReport(reportId) {
|
|
1640
|
+
if (!reportId) {
|
|
1641
|
+
throw new Error('reportId is required');
|
|
1642
|
+
}
|
|
1643
|
+
await this.apiClient.delete(`/api/v1/scheduled-reports/${encodeURIComponent(reportId)}`);
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1454
1646
|
/**
|
|
1455
1647
|
* 메시지 목록 조회
|
|
1456
1648
|
* @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
|
/**
|
|
@@ -397,15 +399,14 @@ export const WebSocketPaths = {
|
|
|
397
399
|
// 서버가 convertAndSendToUser(userId, "/queue/mail", MAIL_CHANGED) 로 전송 — 새 메일이면 messages[] 미리보기 포함
|
|
398
400
|
MAIL_USER_DESTINATION: '/user/queue/mail',
|
|
399
401
|
|
|
400
|
-
//
|
|
402
|
+
// 통화 이벤트 — 서버만 보낸다(통화방 토픽은 종료, 개인 큐는 초대·응답·거절·취소)
|
|
401
403
|
getWebRTCDestination: (roomId) => `/topic/webrtc/${roomId}`,
|
|
402
404
|
getWebRTCUserDestination: () => `/user/queue/webrtc`,
|
|
403
405
|
|
|
404
406
|
// Send destinations
|
|
405
407
|
CHAT_SEND: '/app/chat/send',
|
|
406
408
|
CHAT_READ: '/app/chat/read',
|
|
407
|
-
CHAT_TYPING: '/app/chat/typing'
|
|
408
|
-
WEBRTC_SIGNAL: '/app/webrtc/signal'
|
|
409
|
+
CHAT_TYPING: '/app/chat/typing'
|
|
409
410
|
};
|
|
410
411
|
|
|
411
412
|
/**
|
|
@@ -465,11 +466,8 @@ export const DefaultConfig = {
|
|
|
465
466
|
// API
|
|
466
467
|
apiTimeout: 30000,
|
|
467
468
|
|
|
468
|
-
//
|
|
469
|
-
|
|
470
|
-
{ urls: 'stun:stun.l.google.com:19302' },
|
|
471
|
-
{ urls: 'stun:stun1.l.google.com:19302' }
|
|
472
|
-
],
|
|
469
|
+
// 통화 — 받는 쪽 울림을 끄는 시각(서버 울림 상한 60초 + 여유). 서버 «응답함»·«취소» 가 먼저 오면 그때 끈다.
|
|
470
|
+
callRingTimeoutMs: 65000,
|
|
473
471
|
|
|
474
472
|
// Logging
|
|
475
473
|
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,7 +41,9 @@ 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,
|
|
@@ -81,10 +83,8 @@ export { default as ChatClient } from './chat/ChatClient.js';
|
|
|
81
83
|
// 방 미독 계산기 — ROOM_UNREAD_UPDATED / MESSAGE_RECEIVED 를 순서 역전에 안전하게 합산(READ_SYNC_DESIGN §6).
|
|
82
84
|
export { RoomUnreadReconciler } from './chat/RoomUnreadReconciler.js';
|
|
83
85
|
|
|
84
|
-
//
|
|
86
|
+
// 화상통화 (LiveKit SFU)
|
|
85
87
|
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
88
|
|
|
89
89
|
// Push — PushError/PushErrorCode 는 호출자가 instanceof / code 분기에 사용 가능하도록 noamed export.
|
|
90
90
|
export { default as PushManager, PushError, PushErrorCode } from './push/PushManager.js';
|
|
@@ -22,6 +22,18 @@ const CHAT_DELEGATE_METHODS = [
|
|
|
22
22
|
'getMessages',
|
|
23
23
|
// 파일함 — 참여 중인 방들의 파일 목록(방 하나로 거르기 가능).
|
|
24
24
|
'getFiles',
|
|
25
|
+
// PM 보고서 — 참여 중인 방들에 PM 이 올린 작업 결과(지금은 자료조사) 목록·상세.
|
|
26
|
+
'getPmReports',
|
|
27
|
+
'getPmReport',
|
|
28
|
+
// 예약 보고 — 정해 둔 시각마다 PM 이 전용 방에 올리는 보고의 만들기·고치기·지우기와 고르기 목록.
|
|
29
|
+
'getScheduledReports',
|
|
30
|
+
'getScheduledReport',
|
|
31
|
+
'getScheduledReportKinds',
|
|
32
|
+
'getScheduledReportSourceRooms',
|
|
33
|
+
'getScheduledReportDeliveryRooms',
|
|
34
|
+
'createScheduledReport',
|
|
35
|
+
'updateScheduledReport',
|
|
36
|
+
'deleteScheduledReport',
|
|
25
37
|
'fetchLinkPreview',
|
|
26
38
|
'sendMessage',
|
|
27
39
|
'sendMessageOptimistic',
|
|
@@ -96,41 +108,28 @@ const CHAT_DELEGATE_METHODS = [
|
|
|
96
108
|
];
|
|
97
109
|
|
|
98
110
|
const WEBRTC_DELEGATE_METHODS = [
|
|
99
|
-
'initializeIceServers',
|
|
100
|
-
'getTurnCredentials',
|
|
101
|
-
'createCallRoom',
|
|
102
|
-
'getCallRoom',
|
|
103
|
-
'joinCallRoomApi',
|
|
104
|
-
'leaveCallRoomApi',
|
|
105
111
|
'enableIncomingCalls',
|
|
106
112
|
'disableIncomingCalls',
|
|
107
113
|
'isIncomingCallsEnabled',
|
|
108
|
-
'startCall',
|
|
109
114
|
'callUser',
|
|
115
|
+
'createGroupCall',
|
|
110
116
|
'acceptCall',
|
|
117
|
+
'joinCall',
|
|
111
118
|
'rejectCall',
|
|
112
119
|
'cancelCall',
|
|
113
|
-
'
|
|
114
|
-
'
|
|
115
|
-
'
|
|
116
|
-
'
|
|
117
|
-
'setAudioEnabled',
|
|
120
|
+
'leaveCall',
|
|
121
|
+
'endCallForAll',
|
|
122
|
+
'setCameraEnabled',
|
|
123
|
+
'setMicrophoneEnabled',
|
|
118
124
|
'startScreenShare',
|
|
119
125
|
'stopScreenShare',
|
|
120
|
-
'getLocalStream',
|
|
121
126
|
'getDevices',
|
|
122
127
|
'switchDevice',
|
|
123
|
-
'
|
|
124
|
-
'
|
|
125
|
-
'applyVideoConstraints',
|
|
126
|
-
'getVideoSettings',
|
|
127
|
-
'getAudioSettings',
|
|
128
|
+
'getCallRoom',
|
|
129
|
+
'getParticipation',
|
|
128
130
|
'isInCall',
|
|
129
|
-
'
|
|
130
|
-
'
|
|
131
|
-
'getMediaState',
|
|
132
|
-
'getConnectionSummary',
|
|
133
|
-
'getStats'
|
|
131
|
+
'getCurrentCall',
|
|
132
|
+
'getRingingCalls'
|
|
134
133
|
];
|
|
135
134
|
|
|
136
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
|
}
|