@vibexnpm/talkx 8.7.0 → 8.9.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibexnpm/talkx",
3
- "version": "8.7.0",
3
+ "version": "8.9.0",
4
4
  "type": "module",
5
5
  "description": "TalkFlow SDK - Chat & WebRTC unified client",
6
6
  "main": "dist/talkflow-sdk.umd.js",
@@ -211,6 +211,15 @@ class TalkFlowClient extends EventEmitter {
211
211
  return this.connectionManager ? this.connectionManager.isConnected() : false;
212
212
  }
213
213
 
214
+ /**
215
+ * 회사 구독 대기 중인가 — 서버가 웹소켓을 NOT_SUBSCRIBED 로 거부해 연결을 멈췄다(결제 전·해지 회사).
216
+ * 결제가 확인되면(`GET /api/v1/company/billing` 의 `subscription === 'ACTIVE'`) 같은 토큰으로 {@link #connect} 를 다시 부른다.
217
+ * @returns {boolean}
218
+ */
219
+ isSubscriptionRequired() {
220
+ return this.connectionManager ? this.connectionManager.isSubscriptionRequired() : false;
221
+ }
222
+
214
223
  _enableRefreshSession() {
215
224
  if (this.authSession) return;
216
225
  this.options.sessionMode = 'refresh';
@@ -147,6 +147,7 @@ class ChatClient extends EventEmitter {
147
147
  PLANNING: '기획 중…',
148
148
  DOCUMENT_ANALYSIS: '문서 분석 중…',
149
149
  VIDEO_ANALYSIS: '영상 분석 중…',
150
+ WEB_READING: '페이지 읽는 중…',
150
151
  WRITING: '작성 중…'
151
152
  };
152
153
  // PHASE 의 expiresAt 누락/이상 시 fallback TTL (ms) — 서버 ttl(최소 30s) 보다 약간 길게.
package/src/constants.js CHANGED
@@ -22,6 +22,8 @@ export const ErrorTypes = {
22
22
  CONNECTION_FAILED: 'CONNECTION_FAILED',
23
23
  CONNECTION_LOST: 'CONNECTION_LOST',
24
24
  CONNECTION_TIMEOUT: 'CONNECTION_TIMEOUT',
25
+ // 회사 구독 없음 — 서버가 NOT_SUBSCRIBED 로 거부했다(결제 전·해지). 재연결하지 않고 connect() 를 기다린다.
26
+ SUBSCRIPTION_REQUIRED: 'SUBSCRIPTION_REQUIRED',
25
27
 
26
28
  // Authentication errors
27
29
  JWT_INVALID: 'JWT_INVALID',
@@ -10,6 +10,11 @@ import { ConnectionState, ErrorTypes, WebSocketPaths, DefaultConfig, LogLevel }
10
10
  import { Client as StompClient, ReconnectionTimeMode, StompSocketState } from '@stomp/stompjs';
11
11
  import SockJS from 'sockjs-client';
12
12
 
13
+ /** 서버 구독 관문의 거부 코드(STOMP ERROR 본문 code · HTTP 402) — 결제 전·해지 회사. */
14
+ const SUBSCRIPTION_REQUIRED_CODE = 'NOT_SUBSCRIBED';
15
+ /** 서버가 구독 상태를 잠시 확인하지 못했다(503) — 대기가 아니라 일시 실패다. */
16
+ const SUBSCRIPTION_CHECK_UNAVAILABLE_CODE = 'SUBSCRIPTION_CHECK_UNAVAILABLE';
17
+
13
18
  // STOMP Client 가져오기 (CDN 사용 시 전역 객체 우선)
14
19
  const getStompClient = () => {
15
20
  // noinspection JSUnresolvedReference
@@ -82,6 +87,9 @@ class ConnectionManager extends EventEmitter {
82
87
  this._recoveryTimer = null;
83
88
  this._terminalAuthError = false;
84
89
  this._recoveryEligible = false;
90
+ // 회사 구독 대기 — 서버가 CONNECT·SUBSCRIBE·SEND 를 NOT_SUBSCRIBED 로 거부했다(결제 전·해지 회사).
91
+ // 인증 거부와 다르다: 토큰은 그대로 유효하고, 결제가 끝나면 앱이 같은 토큰으로 connect() 를 다시 부른다.
92
+ this._subscriptionRequired = false;
85
93
 
86
94
  this._installLifecycleListeners();
87
95
  }
@@ -153,7 +161,7 @@ class ConnectionManager extends EventEmitter {
153
161
  * @private
154
162
  */
155
163
  _onResume(reason) {
156
- if (!this.resumeOnVisibility || this._intentionalDisconnect || this._terminalAuthError) {
164
+ if (!this.resumeOnVisibility || this._intentionalDisconnect || this._terminalAuthError || this._subscriptionRequired) {
157
165
  return;
158
166
  }
159
167
 
@@ -248,7 +256,10 @@ class ConnectionManager extends EventEmitter {
248
256
  this.stompClient = null;
249
257
  }
250
258
 
251
- if (this._intentionalDisconnect || this._terminalAuthError) return;
259
+ // 구독 대기면 다시 붙지 않는다 — refresh 세션의 토큰 갱신(AuthSession 'access-token-renewed')·저빈도 복구·탭 복귀가 모두
260
+ // 이리로 오고, 정리를 기다리는 동안 옛 클라이언트가 NOT_SUBSCRIBED 로 대기에 들어간 경우도 여기서 걸린다. 대기는 앱이 connect() 를
261
+ // 다시 불러야 풀린다.
262
+ if (this._intentionalDisconnect || this._terminalAuthError || this._subscriptionRequired) return;
252
263
 
253
264
  // connect() 가 CONNECTED/CONNECTING 이면 조기 반환하므로 상태를 먼저 내려둔다.
254
265
  this._setState(ConnectionState.DISCONNECTED);
@@ -407,6 +418,8 @@ class ConnectionManager extends EventEmitter {
407
418
  }
408
419
  this._cancelReconnectRecovery();
409
420
  this._terminalAuthError = false;
421
+ // 구독 대기는 앱이 connect() 를 다시 불러야 풀린다(결제 확인 뒤). 아직 구독이 없으면 서버가 다시 거부해 대기로 돌아온다.
422
+ this._subscriptionRequired = false;
410
423
  this._setState(ConnectionState.CONNECTING);
411
424
  this.reconnectAttempts = 0;
412
425
  this._intentionalDisconnect = false;
@@ -476,10 +489,20 @@ class ConnectionManager extends EventEmitter {
476
489
  * @private
477
490
  */
478
491
  _onStompError(frame) {
492
+ let code = null;
493
+ let serverMessage = null;
479
494
  try {
480
495
  const payload = JSON.parse(frame.body || frame.headers?.message || '{}');
481
- this._authRejected = ['ACCESS_UNAUTHORIZED', 'SESSION_REVOKED', 'TOKEN_INVALID'].includes(payload.code);
496
+ code = payload.code ?? null;
497
+ serverMessage = typeof payload.message === 'string' ? payload.message : null;
498
+ this._authRejected = ['ACCESS_UNAUTHORIZED', 'SESSION_REVOKED', 'TOKEN_INVALID'].includes(code);
482
499
  } catch { /* Non-authentication STOMP errors keep the normal reconnect policy. */ }
500
+ // 회사 구독 없음 — 재연결해도 같은 거부다. 인증 거부로 기록하지 않고 «구독 대기» 로 멈춘다.
501
+ // 구독 확인 일시 실패(SUBSCRIPTION_CHECK_UNAVAILABLE)는 아래 기본 경로 — 평소처럼 다시 붙는다.
502
+ if (code === SUBSCRIPTION_REQUIRED_CODE) {
503
+ this._stopForSubscription(serverMessage);
504
+ return;
505
+ }
483
506
  this.logger.error('STOMP error:', frame);
484
507
  this._setState(ConnectionState.ERROR);
485
508
 
@@ -490,7 +513,9 @@ class ConnectionManager extends EventEmitter {
490
513
  };
491
514
 
492
515
  if (this._authRejected) this._recordAuthRejection();
493
- const terminal = !this._canRecoverConnection();
516
+ // 구독 확인 일시 실패는 첫 연결이라도 대기 중인 connect() 를 끝내지 않는다 — STOMP 가 곧 다시 붙는다. 여기서 reject 하면
517
+ // 앱은 실패로 알고 방 목록 구독 등 후속 초기화를 건너뛰는데 연결은 붙어 «반쪽 연결» 이 된다.
518
+ const terminal = code !== SUBSCRIPTION_CHECK_UNAVAILABLE_CODE && !this._canRecoverConnection();
494
519
  if (terminal) this._drainPendingSubscriptions(`STOMP error: ${error.message}`);
495
520
 
496
521
  this.emit('error', error);
@@ -512,7 +537,7 @@ class ConnectionManager extends EventEmitter {
512
537
 
513
538
  // forceReconnect 의 의도적 teardown 이면 재연결 시도로 집계하지 않는다 —
514
539
  // 카운터가 오염되면 복귀 직후 다시 max 를 소진해버린다.
515
- if (this._tearingDown || this._intentionalDisconnect || this._terminalAuthError) {
540
+ if (this._tearingDown || this._intentionalDisconnect || this._terminalAuthError || this._subscriptionRequired) {
516
541
  this.logger.debug('WebSocket closed during intentional teardown — not counting as reconnect');
517
542
  return;
518
543
  }
@@ -618,7 +643,43 @@ class ConnectionManager extends EventEmitter {
618
643
 
619
644
  _canRecoverConnection() {
620
645
  return this._recoveryEligible && !this._intentionalDisconnect && !this._terminalAuthError
621
- && this.reconnectRecoveryDelay > 0;
646
+ && !this._subscriptionRequired && this.reconnectRecoveryDelay > 0;
647
+ }
648
+
649
+ /** 회사 구독 대기 중인가 — 서버가 NOT_SUBSCRIBED 로 거부해 연결을 멈췄다. 앱이 connect() 를 다시 부르면 풀린다. */
650
+ isSubscriptionRequired() {
651
+ return this._subscriptionRequired;
652
+ }
653
+
654
+ /**
655
+ * «구독 대기» 로 멈춘다 (서버 구독 관문 설계 v3 §7).
656
+ *
657
+ * <p>인증 정지({@link #_stopForAuthError})와 같은 방식으로 STOMP 재연결 루프·저빈도 복구·탭 복귀 재연결을 끄되, 토큰은
658
+ * 거부된 토큰으로 기록하지 않는다 — 토큰 갱신·로그인은 그대로 둔다. 대기 중인 connect()·구독은 reject 하고, 한 번 들어올 때
659
+ * {@code subscriptionRequired} 를 알린다(일반 {@code error} 는 내지 않는다 — 연결 오류 안내가 결제 안내를 가리지 않게).
660
+ * 연결 중에 해지되면 다음 SUBSCRIBE·SEND 의 ERROR 로 같은 경로에 온다.</p>
661
+ *
662
+ * @private
663
+ */
664
+ _stopForSubscription(serverMessage) {
665
+ const entering = !this._subscriptionRequired;
666
+ this._subscriptionRequired = true;
667
+ this._cancelReconnectRecovery();
668
+ this._setState(ConnectionState.ERROR);
669
+ // 본문의 안내 문구 — message 헤더는 오류 JSON 전체가 이스케이프된 채 오므로 쓰지 않는다.
670
+ const message = serverMessage || 'Company subscription required';
671
+ const error = Object.assign(new Error('Company subscription required'),
672
+ { code: SUBSCRIPTION_REQUIRED_CODE, type: ErrorTypes.SUBSCRIPTION_REQUIRED });
673
+ this._connectReject?.(error);
674
+ this._connectResolve = this._connectReject = null;
675
+ this._drainPendingSubscriptions('Company subscription required');
676
+ const client = this.stompClient;
677
+ Promise.resolve(client?.deactivate()).catch(failure => this.logger.warn('Subscription teardown failed:', failure));
678
+ this.logger.warn('Company subscription required — connection stopped until connect() is called again');
679
+ if (entering) {
680
+ // 정지 전이를 마친 뒤 알린다 — 처리기가 곧바로 connect() 를 불러도 정리 중인 연결과 섞이지 않는다.
681
+ Promise.resolve().then(() => this.emit('subscriptionRequired', { code: SUBSCRIPTION_REQUIRED_CODE, message }));
682
+ }
622
683
  }
623
684
 
624
685
  /** 기존에 연결됐던 클라이언트만 긴 장애 후 탭 이벤트 없이 다시 시도한다. */
@@ -795,6 +856,7 @@ class ConnectionManager extends EventEmitter {
795
856
  // 명시적 해제 — 포그라운드 복귀가 이걸 되살리면 안 된다.
796
857
  this._intentionalDisconnect = true;
797
858
  this._recoveryEligible = false;
859
+ this._subscriptionRequired = false;
798
860
  this._authRejected = false;
799
861
  this._rejectedToken = null;
800
862
  this._cancelReconnectRecovery();
@@ -7,6 +7,8 @@ const CONNECTION_EVENT_MAP = [
7
7
  // 재연결 '성공' + 포그라운드 복귀 감지 (RECONNECT_SYNC_DESIGN §6.1/§6.4)
8
8
  ['reconnected', 'reconnected'],
9
9
  ['resumed', 'resumed'],
10
+ // 회사 구독 대기(서버 구독 관문) — 연결 오류와 따로 알린다. 결제 뒤 connect() 를 다시 부르면 풀린다.
11
+ ['subscriptionRequired', 'subscriptionRequired'],
10
12
  ['error', 'connectionError']
11
13
  ];
12
14
 
@@ -0,0 +1,18 @@
1
+ /**
2
+ * 웹페이지 읽기 진행 단계(8.9.0) — `WEB_READING` 이 phase 유니온에 있다는 것을 tsc 로 고정한다.
3
+ *
4
+ * <p>phase 를 직접 분기하는 화면이 이 값을 쓸 수 있어야 한다. 기본 라벨은 «페이지 읽는 중…» 이다.</p>
5
+ */
6
+ import type { AssistantProgressEvent, AssistantRoomProgressEvent, AssistantProgressStreamSnapshot } from '../index';
7
+
8
+ export function isWebReading(event: AssistantProgressEvent): boolean {
9
+ return event.active && event.phase === 'WEB_READING';
10
+ }
11
+
12
+ export function roomIsWebReading(event: AssistantRoomProgressEvent): boolean {
13
+ return event.phase === 'WEB_READING';
14
+ }
15
+
16
+ export function snapshotIsWebReading(snapshot: AssistantProgressStreamSnapshot): boolean {
17
+ return snapshot.phase === 'WEB_READING';
18
+ }
package/types/index.d.ts CHANGED
@@ -26,6 +26,8 @@ export const ErrorTypes: {
26
26
  readonly CONNECTION_FAILED: 'CONNECTION_FAILED';
27
27
  readonly CONNECTION_LOST: 'CONNECTION_LOST';
28
28
  readonly CONNECTION_TIMEOUT: 'CONNECTION_TIMEOUT';
29
+ /** 회사 구독 없음(결제 전·해지) — 웹소켓이 멈췄다. 결제 확인 뒤 `connect()` 를 다시 부른다. */
30
+ readonly SUBSCRIPTION_REQUIRED: 'SUBSCRIPTION_REQUIRED';
29
31
  readonly JWT_INVALID: 'JWT_INVALID';
30
32
  readonly JWT_EXPIRED: 'JWT_EXPIRED';
31
33
  readonly JWT_PARSE_FAILED: 'JWT_PARSE_FAILED';
@@ -1507,7 +1509,7 @@ export interface AssistantProgressEvent {
1507
1509
  /** 진행 주체 페르소나 ID. */
1508
1510
  personaId: string | null;
1509
1511
  /** 진행 단계 — `active:true` 일 때. 종료 시 null. */
1510
- phase: 'THINKING' | 'SEARCHING' | 'PLANNING' | 'DOCUMENT_ANALYSIS' | 'VIDEO_ANALYSIS' | 'WRITING' | null;
1512
+ phase: 'THINKING' | 'SEARCHING' | 'PLANNING' | 'DOCUMENT_ANALYSIS' | 'VIDEO_ANALYSIS' | 'WEB_READING' | 'WRITING' | null;
1511
1513
  /** DOCUMENT_ANALYSIS의 실측 진행률. 같은 phase heartbeat 사이와 재입장 후에도 보존된다. */
1512
1514
  progress?: DocumentAnalysisProgress | null;
1513
1515
  /** phase 의 기본 표시 라벨(예: "생각 중…"). SDK 매핑 — 커스텀하려면 `phase` 사용. 종료 시 null. */
@@ -1578,7 +1580,7 @@ export interface AssistantRoomProgressEvent {
1578
1580
  /** 표시 기준이 된 stream (가장 최근 갱신). 없으면 null. */
1579
1581
  streamId: string | null;
1580
1582
  personaId: string | null;
1581
- phase: 'THINKING' | 'SEARCHING' | 'PLANNING' | 'DOCUMENT_ANALYSIS' | 'VIDEO_ANALYSIS' | 'WRITING' | null;
1583
+ phase: 'THINKING' | 'SEARCHING' | 'PLANNING' | 'DOCUMENT_ANALYSIS' | 'VIDEO_ANALYSIS' | 'WEB_READING' | 'WRITING' | null;
1582
1584
  progress?: DocumentAnalysisProgress | null;
1583
1585
  label: string | null;
1584
1586
  /** 표시 기준 stream 의 누적 본문. `textAvailable:false` 거나 진행 중이 아니면 null. */
@@ -1591,7 +1593,7 @@ export interface AssistantRoomProgressEvent {
1591
1593
  export interface AssistantProgressStreamSnapshot {
1592
1594
  streamId: string;
1593
1595
  personaId: string | null;
1594
- phase: 'THINKING' | 'SEARCHING' | 'PLANNING' | 'DOCUMENT_ANALYSIS' | 'VIDEO_ANALYSIS' | 'WRITING' | null;
1596
+ phase: 'THINKING' | 'SEARCHING' | 'PLANNING' | 'DOCUMENT_ANALYSIS' | 'VIDEO_ANALYSIS' | 'WEB_READING' | 'WRITING' | null;
1595
1597
  progress?: DocumentAnalysisProgress | null;
1596
1598
  label: string | null;
1597
1599
  text: string | null;
@@ -1997,7 +1999,7 @@ export interface RetentionCleanupPayload {
1997
1999
  /**
1998
2000
  * TalkFlowClient 이벤트 맵 — 런타임 {@code TalkFlowClient.js} 의 {@code this.emit(...)} 호출 기반.
1999
2001
  *
2000
- * <p><b>Connection 관련</b>: stateChange, connected, disconnected, reconnecting, reconnected, resumed, connectionError, tokenSet, loggedOut</p>
2002
+ * <p><b>Connection 관련</b>: stateChange, connected, disconnected, reconnecting, reconnected, resumed, subscriptionRequired, connectionError, tokenSet, loggedOut</p>
2001
2003
  * <p><b>Chat 관련 (ChatClient 이벤트를 이 이름으로 re-emit)</b>: chatMessage, newChatMessage, messageUpdated, messageDeleted, reactionChanged, linkPreviewAttached, messageRead, typing, assistantProgress, memberJoined, memberLeft, roomSubscribed, roomUnsubscribed, roomListSubscribed, roomListUnsubscribed, roomListUpdate, roomListMessage, roomListCreated, roomListJoined, roomListLeft, roomListSelfLeft, roomListRoomUpdated, retentionCleanup</p>
2002
2004
  * <p><b>Push 관련</b>: pushEnabled, pushFailed, pushNotification</p>
2003
2005
  * <p><b>WebRTC 관련</b>: localStreamStarted, localStreamStopped, remoteTrack, screenShareStarted, screenShareEnded, deviceChange, mediaStateChanged, callStarted, callEnded, callRequested, callAccepted, callRejected, callCancelled, callInvitation, callBusy, incomingCall, incomingCallWhileBusy, userJoined, userLeft, participantLeft, participantMediaState, peerConnected, peerDisconnected, peerClosed, webrtcError</p>
@@ -2022,6 +2024,15 @@ export interface TalkFlowClientEvents {
2022
2024
  * 발행된다(두 이벤트가 겹치지 않아 재동기화가 중복 실행되지 않는다).</p>
2023
2025
  */
2024
2026
  resumed: { reason: 'visibilitychange' | 'pageshow' | 'online' };
2027
+ /**
2028
+ * 회사 구독 없음 — 서버가 웹소켓 연결·구독·전송을 `NOT_SUBSCRIBED` 로 거부했다(결제 전·해지 회사). SDK 는 재연결하지 않고
2029
+ * 토큰도 거부된 것으로 기록하지 않는다(로그인·토큰 갱신은 그대로). 대기에 들어갈 때 한 번 발행되며 `connectionError` 는 내지 않는다.
2030
+ *
2031
+ * <p>앱: 파트너면 결제 화면, 그 밖은 «회사 구독이 필요합니다. 관리자에게 문의하세요.» 안내. 결제가 확인되면
2032
+ * (`subscription === 'ACTIVE'`) 같은 토큰으로 `connect()` 를 다시 부른다. 구독 확인 일시 실패(`SUBSCRIPTION_CHECK_UNAVAILABLE`)는
2033
+ * 이 이벤트가 아니다 — 평소처럼 재연결한다.</p>
2034
+ */
2035
+ subscriptionRequired: { code: 'NOT_SUBSCRIBED'; message: string };
2025
2036
  connectionError: unknown;
2026
2037
  tokenSet: { userId: string };
2027
2038
  loggedOut: void;
@@ -2679,6 +2690,8 @@ export default class TalkFlowClient implements SdkEventEmitter<TalkFlowClientEve
2679
2690
  /**
2680
2691
  * 연결 완료까지 기다립니다. 이전 연결의 자동 복구가 이어지는 동안 재시도 한도 소진으로 reject하지 않습니다.
2681
2692
  * 복구 연결 시 resolve하며, 명시적 disconnect·인증 종료·최초 연결 실패 시 reject합니다.
2693
+ * 회사 구독이 없으면(서버 `NOT_SUBSCRIBED`) `code: 'NOT_SUBSCRIBED'`·`type: 'SUBSCRIPTION_REQUIRED'` 로 reject 하고
2694
+ * 재연결하지 않습니다 — {@link TalkFlowClientEvents.subscriptionRequired} 참고.
2682
2695
  */
2683
2696
  connect(jwt?: string, options?: ConnectOptions): Promise<void>;
2684
2697
  disconnect(): Promise<void>;
@@ -2686,6 +2699,11 @@ export default class TalkFlowClient implements SdkEventEmitter<TalkFlowClientEve
2686
2699
  setToken(token: string): Promise<void>;
2687
2700
  updateToken(newToken: string): Promise<void>;
2688
2701
  isConnected(): boolean;
2702
+ /**
2703
+ * 회사 구독 대기 중인가 — 서버가 웹소켓을 `NOT_SUBSCRIBED` 로 거부해 연결을 멈췄다(결제 전·해지 회사).
2704
+ * 결제가 확인되면(`GET /api/v1/company/billing` 의 `subscription === 'ACTIVE'`) 같은 토큰으로 `connect()` 를 다시 부른다.
2705
+ */
2706
+ isSubscriptionRequired(): boolean;
2689
2707
  getState(): ConnectionStateValue;
2690
2708
  hasToken(): boolean;
2691
2709