@vibexnpm/talkx 8.1.0 → 8.4.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 +3 -0
- package/dist/index.d.ts +125 -7
- package/dist/talkflow-sdk.esm.js +1 -1
- package/dist/talkflow-sdk.esm.js.map +1 -1
- package/dist/talkflow-sdk.standalone.js +1 -1
- package/dist/talkflow-sdk.standalone.js.map +1 -1
- package/dist/talkflow-sdk.umd.js +1 -1
- package/dist/talkflow-sdk.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/chat/ChatClient.js +135 -3
- package/src/constants.js +12 -0
- package/src/talkflow/delegates.js +9 -0
- package/src/talkflow/eventForwarding.js +5 -0
- package/types/__typetests__/roomOwnership.types.ts +51 -0
- package/types/__typetests__/scheduleCard.types.ts +18 -2
- package/types/__typetests__/videoAnalysis.types.ts +131 -0
- package/types/index.d.ts +125 -7
package/dist/talkflow-sdk.umd.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("@stomp/stompjs"),require("sockjs-client")):"function"==typeof define&&define.amd?define(["exports","@stomp/stompjs","sockjs-client"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).TalkFlowSDK={},e.StompJs,e.SockJS)}(this,function(e,t,s){"use strict";class i{constructor(){this._events=new Map}on(e,t){return this._events.has(e)||this._events.set(e,new Set),this._events.get(e).add(t),()=>this.off(e,t)}once(e,t){const s=(...i)=>{this.off(e,s),t.apply(this,i)};return s._originalListener=t,this.on(e,s)}off(e,t){const s=this._events.get(e);if(s){for(const e of s)if(e===t||e._originalListener===t){s.delete(e);break}0===s.size&&this._events.delete(e)}}emit(e,t){const s=this._events.get(e);s&&s.forEach(s=>{try{s(t)}catch(t){console.error(`Error in event listener for '${e}':`,t)}})}removeAllListeners(e){e?this._events.delete(e):this._events.clear()}listenerCount(e){const t=this._events.get(e);return t?t.size:0}eventNames(){return Array.from(this._events.keys())}}const n={DISCONNECTED:"disconnected",CONNECTING:"connecting",CONNECTED:"connected",RECONNECTING:"reconnecting",ERROR:"error"},r={CONNECTION_FAILED:"CONNECTION_FAILED",CONNECTION_LOST:"CONNECTION_LOST",CONNECTION_TIMEOUT:"CONNECTION_TIMEOUT",JWT_INVALID:"JWT_INVALID",JWT_EXPIRED:"JWT_EXPIRED",JWT_PARSE_FAILED:"JWT_PARSE_FAILED",UNAUTHORIZED:"UNAUTHORIZED",API_ERROR:"API_ERROR",API_TIMEOUT:"API_TIMEOUT",CHAT_ROOM_NOT_FOUND:"CHAT_ROOM_NOT_FOUND",CHAT_MESSAGE_FAILED:"CHAT_MESSAGE_FAILED",CHAT_SUBSCRIPTION_FAILED:"CHAT_SUBSCRIPTION_FAILED",MEDIA_ACCESS_DENIED:"MEDIA_ACCESS_DENIED",SCREEN_SHARE_DENIED:"SCREEN_SHARE_DENIED",PEER_CONNECTION_FAILED:"PEER_CONNECTION_FAILED",ICE_CONNECTION_FAILED:"ICE_CONNECTION_FAILED",SIGNALING_FAILED:"SIGNALING_FAILED",DEVICE_SWITCH_FAILED:"DEVICE_SWITCH_FAILED",ENUMERATE_DEVICES_FAILED:"ENUMERATE_DEVICES_FAILED",CALL_ROOM_NOT_FOUND:"CALL_ROOM_NOT_FOUND",INVALID_STATE:"INVALID_STATE",UNKNOWN_ERROR:"UNKNOWN_ERROR"},a={CALL_OFFER:"call_offer",CALL_ANSWER:"call_answer",ICE_CANDIDATE:"ice_candidate",JOIN_ROOM:"join_room",LEAVE_ROOM:"leave_room",PEER_JOINED:"peer_joined",PEER_LEFT:"peer_left",VIDEO_STATE_CHANGED:"video_state_changed",AUDIO_STATE_CHANGED:"audio_state_changed",SCREEN_SHARE_STARTED:"screen_share_started",SCREEN_SHARE_ENDED:"screen_share_ended",CALL_REQUEST:"call_request",CALL_ACCEPT:"call_accept",CALL_REJECT:"call_reject",CALL_CANCEL:"call_cancel",CALL_END:"call_end",CALL_INVITATION:"call_invitation",CALL_BUSY:"call_busy"},o={DIRECT:"DIRECT",GROUP:"GROUP",PRIVATE_GROUP:"PRIVATE_GROUP",TEAM:"TEAM"},c={MESSAGE_RECEIVED:"MESSAGE_RECEIVED",MESSAGE_DELETED:"MESSAGE_DELETED",MESSAGE_UPDATED:"MESSAGE_UPDATED",ROOM_CREATED:"ROOM_CREATED",ROOM_JOINED:"ROOM_JOINED",ROOM_LEFT:"ROOM_LEFT",ROOM_UPDATED:"ROOM_UPDATED",ROOM_KICKED:"ROOM_KICKED",ROOM_BANNED:"ROOM_BANNED",MESSAGE_RETENTION_CLEANUP:"MESSAGE_RETENTION_CLEANUP",ROOM_UNREAD_UPDATED:"ROOM_UNREAD_UPDATED"},l={SOCKJS_ENDPOINT:"/ws-chat",NATIVE_ENDPOINT:"/ws-chat-native",APP_PREFIX:"/app",TOPIC_PREFIX:"/topic",QUEUE_PREFIX:"/queue",USER_PREFIX:"/user",getChatDestination:e=>`/topic/chat/${e}`,getChatReadDestination:e=>`/topic/chat/${e}/read`,getChatTypingDestination:e=>`/topic/chat/${e}/typing`,getChatAssistantStreamDestination:e=>`/topic/chat/${e}/assistant-stream`,getChatSummaryDestination:e=>`/topic/chat/${e}/summary`,ROOM_LIST_USER_DESTINATION:"/user/queue/rooms",getWebRTCDestination:e=>`/topic/webrtc/${e}`,getWebRTCUserDestination:()=>"/user/queue/webrtc",CHAT_SEND:"/app/chat/send",CHAT_READ:"/app/chat/read",CHAT_TYPING:"/app/chat/typing",WEBRTC_SIGNAL:"/app/webrtc/signal"},h={DEVELOPMENT:"development",STAGING:"staging",PRODUCTION:"production"},d={[h.DEVELOPMENT]:{serverUrl:"https://dev-chat.apiorbit.net",wsEndpoint:"/ws-chat"},[h.STAGING]:{serverUrl:"https://stg-api.talk-x.app",wsEndpoint:"/ws-chat"},[h.PRODUCTION]:{serverUrl:"https://prod-api.talk-x.app",wsEndpoint:"/ws-chat"}},g={environment:h.PRODUCTION,reconnectDelay:5e3,maxReconnectDelay:3e4,maxReconnectAttempts:10,heartbeatIncoming:1e4,heartbeatOutgoing:1e4,resumeOnVisibility:!0,syncOnReconnect:!0,rewindMillis:3e3,maxSyncPages:5,syncPageSize:100,apiTimeout:3e4,iceServers:[{urls:"stun:stun.l.google.com:19302"},{urls:"stun:stun1.l.google.com:19302"}],logLevel:2};function u(e){return(d[e]||d[h.PRODUCTION]).serverUrl}const p={DEBUG:0,INFO:1,WARN:2,ERROR:3,NONE:4};class m{constructor(e=p.WARN,t="TalkFlow"){this.level=e,this.prefix=t}setLevel(e){this.level=e}setPrefix(e){this.prefix=e}_format(e,...t){return[`[${(new Date).toISOString()}] [${e}] [${this.prefix}]`,...t]}debug(...e){this.level<=p.DEBUG&&console.debug(...this._format("DEBUG",...e))}info(...e){this.level<=p.INFO&&console.info(...this._format("INFO",...e))}warn(...e){this.level<=p.WARN&&console.warn(...this._format("WARN",...e))}error(...e){this.level<=p.ERROR&&console.error(...this._format("ERROR",...e))}}class _{constructor(e){this.baseUrl=e.baseUrl.replace(/\/$/,""),this.apiKey=e.apiKey,this.projectId=e.projectId,this.jwtToken=e.jwtToken,this.timeout=e.timeout||g.apiTimeout,this.logger=new m(e.logLevel,"ApiClient")}setJwtToken(e){this.jwtToken=e}_getHeaders(){const e={"Content-Type":"application/json","X-API-KEY":this.apiKey,"X-PROJECT-ID":this.projectId};return this.jwtToken&&(e.Authorization=this.jwtToken.startsWith("Bearer ")?this.jwtToken:`Bearer ${this.jwtToken}`),e}async request(e,t,s={}){if(!this.authProvider||s.skipAuth)return this._request(e,t,s);this.jwtToken=await this.authProvider.getAccessToken();const i=this.jwtToken,n=this.authProvider.generation;try{return await this._request(e,t,s)}catch(r){if(401===r.status&&["ACCESS_UNAUTHORIZED","TOKEN_INVALID"].includes(r.response?.code)){if(n!==this.authProvider.generation)throw r;this.jwtToken=await this.authProvider.getAccessToken({forceRefresh:!0,rejectedToken:i});const a=this.jwtToken,o=this.authProvider.generation;try{return await this._request(e,t,s)}catch(e){throw await this.authProvider.authenticationFailed(e,{rejectedToken:a,generation:o}),e}}throw await this.authProvider.authenticationFailed(r,{rejectedToken:i,generation:n}),r}}async _request(e,t,s={}){const{body:i,params:n,headers:a}=s;let o=`${this.baseUrl}${t}`;if(n){const e=new URLSearchParams;Object.entries(n).forEach(([t,s])=>{null!=s&&e.append(t,s)});const t=e.toString();t&&(o+=`?${t}`)}const c=new AbortController,l=setTimeout(()=>c.abort(),this.timeout);let h;try{this.logger.debug(`${e} ${o}`),h=await fetch(o,{method:e,headers:{...this._getHeaders(),...a},credentials:this.authProvider?"omit":"same-origin",body:i?JSON.stringify(i):void 0,signal:c.signal})}catch(t){if(clearTimeout(l),"AbortError"===t.name){const e=new Error("Request timeout");throw e.code=r.API_TIMEOUT,e}throw this.logger.error(`API Error: ${e} ${o}`,t),t}clearTimeout(l);const d=h.headers.get("content-type");let g;if(g=d&&d.includes("application/json")?await h.json():await h.text(),!h.ok){const t=new Error(g?.message||`HTTP ${h.status}`);throw t.code=r.API_ERROR,t.status=h.status,t.response=g,this.logger.error(`API Error: ${e} ${o}`,t),t}return this.logger.debug(`Response ${h.status}`),g}get(e,t){return this.request("GET",e,{params:t})}post(e,t,s,i){return this.request("POST",e,{body:t,params:s,headers:i})}put(e,t){return this.request("PUT",e,{body:t})}patch(e,t){return this.request("PATCH",e,{body:t})}delete(e,t){return this.request("DELETE",e,{params:t})}async upload(e,t,s={}){if(!this.authProvider)return this._upload(e,t,s);this.jwtToken=await this.authProvider.getAccessToken();const i=this.jwtToken,n=this.authProvider.generation;try{return await this._upload(e,t,s)}catch(r){if(401===r.status&&["ACCESS_UNAUTHORIZED","TOKEN_INVALID"].includes(r.response?.code)){if(n!==this.authProvider.generation)throw r;this.jwtToken=await this.authProvider.getAccessToken({forceRefresh:!0,rejectedToken:i});const a=this.jwtToken,o=this.authProvider.generation;try{return await this._upload(e,t,s)}catch(e){throw await this.authProvider.authenticationFailed(e,{rejectedToken:a,generation:o}),e}}throw await this.authProvider.authenticationFailed(r,{rejectedToken:i,generation:n}),r}}_upload(e,t,s={}){const{fieldName:i="file",onProgress:n,signal:a,timeout:o=6e5}=s;return new Promise((s,c)=>{const l=`${this.baseUrl}${e}`,h=new XMLHttpRequest,d=new FormData;d.append(i,t),h.open("POST",l),h.timeout=o,h.setRequestHeader("X-API-KEY",this.apiKey),h.setRequestHeader("X-PROJECT-ID",this.projectId),this.jwtToken&&h.setRequestHeader("Authorization",this.jwtToken.startsWith("Bearer ")?this.jwtToken:`Bearer ${this.jwtToken}`),"function"==typeof n&&(h.upload.onprogress=e=>{e.lengthComputable&&n({loaded:e.loaded,total:e.total,percent:Math.round(e.loaded/e.total*100)})});const g=()=>h.abort();if(a){if(a.aborted)return void c(new Error("Upload cancelled"));a.addEventListener("abort",g)}const u=()=>{a&&a.removeEventListener("abort",g)};h.onload=()=>{u();let e=h.responseText;if((h.getResponseHeader("content-type")||"").includes("application/json"))try{e=JSON.parse(h.responseText)}catch{}if(h.status>=200&&h.status<300)this.logger.debug(`Upload ${h.status}:`,e),s(e);else{const t=new Error(e?.message||`HTTP ${h.status}`);t.code=r.API_ERROR,t.status=h.status,t.response=e,this.logger.error(`Upload Error: POST ${l}`,t),c(t)}},h.onerror=()=>{u();const e=new Error("Network error during upload");e.code=r.API_ERROR,this.logger.error(`Upload Network Error: POST ${l}`,e),c(e)},h.ontimeout=()=>{u();const e=new Error("Upload timeout");e.code=r.API_TIMEOUT,this.logger.error(`Upload Timeout: POST ${l}`,e),c(e)},h.onabort=()=>{u(),c(new Error("Upload cancelled"))},this.logger.debug(`POST ${l} (multipart, ${t.size} bytes)`),h.send(d)})}}const S=new Map,E=new Set(["REFRESH_INVALID","REFRESH_REUSED","SESSION_REVOKED","ACCOUNT_BLOCKED","LOGIN_FAILED"]),f=e=>E.has(e?.response?.code||e?.code);function y(e){try{const t=e.replace(/^Bearer\s+/i,"").split(".")[1].replace(/-/g,"+").replace(/_/g,"/");return JSON.parse(atob(t))}catch{return{}}}class b extends i{static forServer(e){const t=e.replace(/\/$/,"");return S.has(t)||S.set(t,new b(t)),S.get(t)}constructor(e){super(),this.serverUrl=e,this.state="checking",this.data=null,this.clients=new Set,this.generation=0,this.refreshPromise=null,this.timer=null,this.peerWaiters=new Map,this.markerKey=`talkflow:auth:${e}`,this.marker=this._readMarker(),this.serverClock=null,this.channel=null,this.storageListener=e=>{if(e.key!==this.markerKey)return;const t=this._readMarker();(t.revision!==this.marker.revision||t.logout)&&(this.marker=t,this.generation++,this._clear(!1).catch(e=>this.emit("authError",e)))},globalThis.addEventListener?.("storage",this.storageListener),"undefined"!=typeof window&&"undefined"!=typeof BroadcastChannel&&(this.channel=new BroadcastChannel(`talkflow:auth:${e}`),this.channel.onmessage=e=>this._receive(e.data),this.channel.unref?.())}_readMarker(){try{const e=globalThis.localStorage?.getItem(this.markerKey);return e?JSON.parse(e):{revision:"initial",logout:!1}}catch{throw this._storageError()}}_storageError(){const e=new Error("Authentication storage is unavailable");return e.code="AUTH_STORAGE_UNAVAILABLE",e}_writeMarker(e){try{if(!globalThis.localStorage)throw this._storageError();globalThis.localStorage.setItem(this.markerKey,JSON.stringify(e)),this.marker=e}catch{throw this._storageError()}}_newMarker(e,t=e){const s=this._readMarker(),i={revision:globalThis.crypto?.randomUUID?.()||String(Date.now())+"-"+Math.random(),logout:e,deviceId:s.deviceId,pending:t,pendingRefreshToken:e&&t?s.refreshToken||s.pendingRefreshToken:void 0};return this._writeMarker(i),i}setDeviceId(e){this._writeMarker({...this._readMarker(),deviceId:e})}_monotonic(){return globalThis.performance?.now?.()??Date.now()}_serverNow(){return this.serverClock?this.serverClock.server+Math.max(this._monotonic()-this.serverClock.mono,Date.now()-this.serverClock.wall):Date.now()}_shareData(){return this.data?{...this.data,refreshToken:void 0,serverTime:this._serverNow()}:null}_publish(e,t,s=this.marker){const{revision:i,logout:n,pending:r}=s;this.channel?.postMessage({type:e,marker:{revision:i,logout:n,pending:r},data:t})}_setState(e,t,s=!1){const i=this.state;this.state=e,(i!==e||t||s)&&this.emit("authStateChanged",{state:e,previous:i,error:t})}getAuthState(){return this.state}isLoggedIn(){return"authenticated"===this.state}_fresh(e=this.data?.accessToken){return 1e3*Number(y(e||"").exp||0)>this._serverNow()+(this.refreshMargin??6e4)}attach(e){this.clients.add(e)}detach(e){this.clients.delete(e),this.schedule()}_enqueue(e){const t=(this.transition||Promise.resolve()).then(e);return this.transition=t.catch(()=>{}),t}_accept(e,t=!0){const s=this.generation,i=this.marker;return this._enqueue(async()=>{if(s!==this.generation)throw this._cancelled();if(!(e?.accessToken&&e?.projectId&&e?.clientKey&&e?.user?.userId)){const e=new Error("Invalid authentication response");throw e.code="AUTH_UNAVAILABLE",e}const n=this.data&&(this.data.user.userId!==e.user.userId||this.data.projectId!==e.projectId);if(t&&e.refreshToken){const t=this._readMarker();if(t.revision!==i.revision||t.logout)throw this._cancelled();this._writeMarker({...t,refreshToken:e.refreshToken,refreshRequestId:void 0,pending:!1,pendingRefreshToken:void 0})}if(Number.isFinite(e.serverTime)&&(this.serverClock={server:e.serverTime+(e._roundTripMillis||0),mono:this._monotonic(),wall:Date.now()}),this.refreshMargin=Math.min(6e4,Math.max(0,1e3*Number(y(e.accessToken).exp)-this._serverNow())/10),this.data={...e,refreshToken:void 0},await Promise.all([...this.clients].map(t=>t._acceptAuthSession(e))),s!==this.generation)throw this._cancelled();return this._setState("authenticated",void 0,n),t&&this._publish("tokens",this._shareData(),this.marker),this._reconnectClients(),this.schedule(),e})}_reconnectClients(){for(const e of this.clients)if(e._authConnectionRequested&&e.isConnected?.()){const t=e.connectionManager;t?._authenticatedToken!==this.data?.accessToken&&t.forceReconnect("access-token-renewed").catch(e=>this.emit("authError",e))}}_clear(e=!0){const t=this.marker;return this._enqueue(async()=>{this.data=null,clearTimeout(this.timer),this.timer=null,this._setState("unauthenticated"),await Promise.all([...this.clients].map(e=>e._clearAuthSession())),e&&this._publish("logout",void 0,t)})}async _receive(e){if(e?.marker)try{const t=this._readMarker();if(e.marker.revision!==t.revision&&t.revision!==this.marker.revision)return;if("logout"===e.type||"login-start"===e.type){if(e.marker.revision!==t.revision)return;return this.marker=e.marker,this.generation++,await this._clear(!1),void("login-start"===e.type&&this._setState("checking"))}if(e.marker.revision!==this._readMarker().revision||e.marker.logout)return;if("request-token"===e.type&&this._fresh())this._publish("peer-token",{requestId:e.data.requestId,session:this._shareData()});else if("peer-token"===e.type)this.peerWaiters.get(e.data.requestId)?.(e.data.session);else if("tokens"===e.type){this.marker.revision!==e.marker.revision&&this.generation++,this.marker=e.marker;const t=y(e.data?.accessToken||""),s=y(this.data?.accessToken||"");if(t.sid===s.sid&&t.exp<s.exp)return;await this._accept(e.data,!1)}}catch(e){this.emit("authError",e)}}async _peerToken(e){if(!this.channel)return null;const t=`${Date.now()}-${Math.random()}`;return new Promise(s=>{const i=i=>{i&&(i.accessToken===e||1e3*Number(y(i.accessToken).exp)<=Number(i.serverTime)+6e4)||(clearTimeout(n),this.peerWaiters.delete(t),s(i))},n=setTimeout(()=>i(null),100);this.peerWaiters.set(t,i),this._publish("request-token",{requestId:t})})}_locked(e){const t="undefined"!=typeof navigator?navigator.locks:null,s=(this.httpQueue||Promise.resolve()).then(()=>t?.request?t.request(`talkflow:auth:${this.serverUrl}`,e):e());return this.httpQueue=s.catch(()=>{}),s}_newRefreshRequestId(){const e=new Uint8Array(32);if(!globalThis.crypto?.getRandomValues){const e=new Error("Secure randomness is unavailable");throw e.code="AUTH_RANDOM_UNAVAILABLE",e}return globalThis.crypto.getRandomValues(e),Array.from(e,e=>e.toString(16).padStart(2,"0")).join("")}async _request(e,t,s={}){const i=this._monotonic(),n=new AbortController,r=setTimeout(()=>n.abort(),15e3);try{const r=await fetch(`${this.serverUrl}${e}`,{method:"POST",credentials:"omit",signal:n.signal,headers:{"Content-Type":"application/json","X-TalkFlow-Session":"refresh",...s},body:void 0===t?void 0:JSON.stringify(t)}),a=r.headers.get("content-type")?.includes("application/json")?await r.json():await r.text();if(!r.ok){const e=new Error(a?.message||`HTTP ${r.status}`);throw e.status=r.status,e.code=a?.code,e.response=a,e}const o=void 0!==a?.data?a.data:a;return o&&"object"==typeof o?Object.defineProperty({...o},"_roundTripMillis",{value:this._monotonic()-i}):o}finally{clearTimeout(r)}}async login({email:e,password:t}){const s=++this.generation;return this._locked(async()=>{const i=this._readMarker();this._writeMarker(i),i.pendingRefreshToken&&await this._request("/v1/auth/signout",{refreshToken:i.pendingRefreshToken,deviceId:i.deviceId});const n=await this._request("/api/v1/auth/login",{email:e,password:t,previousRefreshToken:i.refreshToken});if(!n?.refreshToken){const e=new Error("Server does not support the refresh session contract");throw e.code="AUTH_PROTOCOL_UNSUPPORTED",e}if(s!==this.generation||this._readMarker().revision!==i.revision)throw await this._request("/v1/auth/signout",{refreshToken:n.refreshToken}),this._cancelled();return this._newMarker(!1),this._accept(n)})}async restoreSession(){if(this._readMarker().logout)return this._readMarker().pending?await this.logout():await this._clear(!1),null;if(this._fresh())return this.data;if(!this._readMarker().refreshToken)return await this._clear(!1),null;this.data||this._setState("checking");try{return await this.getAccessToken(),this.data}catch(e){if(f(e))return null;throw e}}async getAccessToken({forceRefresh:e=!1,rejectedToken:t}={}){if(this._readMarker().logout)throw this._loggedOut();if(this._fresh()&&(!e||t&&t!==this.data.accessToken))return this.data.accessToken;if(this.refreshPromise)return this.refreshPromise;const s=this.generation,i=this._readMarker().revision,n=this._locked(async()=>{if(s!==this.generation||i!==this._readMarker().revision)throw this._cancelled();if(this._fresh()&&(!e||t&&t!==this.data.accessToken))return this.data.accessToken;if(this.marker=this._readMarker(),!this.marker.refreshToken)throw this._loggedOut();this._writeMarker(this.marker);const n=await this._peerToken(t);let r=n;if(!r){const e=this._readMarker();if(s!==this.generation||i!==e.revision||e.logout)throw this._cancelled();const t=e.refreshRequestId||this._newRefreshRequestId();this._writeMarker({...e,refreshRequestId:t}),r=await this._request("/api/v1/auth/refresh",{refreshToken:e.refreshToken,requestId:t})}if(s!==this.generation||i!==this._readMarker().revision)throw this._cancelled();return await this._accept(r,!n),r.accessToken}).catch(async e=>{throw s===this.generation&&i===this._readMarker().revision&&(f(e)?await this.authenticationFailed(e):this.data?this.emit("authError",e):this._setState("unavailable",e)),e}).finally(()=>{this.refreshPromise===n&&(this.refreshPromise=null)});return this.refreshPromise=n,n}async logout(){const e=this.data?.accessToken;this.generation++;const t=this._newMarker(!0),s=this._clear(),i=this._locked(async()=>{const s=await this._request("/v1/auth/signout",{refreshToken:t.pendingRefreshToken,deviceId:t.deviceId},e?{Authorization:"Bearer "+e.replace(/^Bearer\s+/i,"")}:{});return this._readMarker().revision===t.revision&&this._writeMarker({...t,pending:!1,pendingRefreshToken:void 0}),s});await Promise.all([s,i]),this._readMarker().revision===t.revision&&this._writeMarker({...t,pending:!1,pendingRefreshToken:void 0})}async changePassword(e){await this.getAccessToken();const t=++this.generation;return this._locked(async()=>{const s=await e();if(t!==this.generation)throw this._cancelled();const i=s?.data??s;return i?.reauthenticationRequired?(this._newMarker(!0,!1),await this._clear(),s):(this._newMarker(!1),await this._accept(i),s)}).catch(async e=>{throw t===this.generation&&await this.authenticationFailed(e),e})}_loggedOut(){const e=new Error("Sign in is required");return e.code="SESSION_REVOKED",e}async authenticationFailed(e,{rejectedToken:t,generation:s}={}){void 0!==s&&s!==this.generation||t&&this.data&&t!==this.data.accessToken||f(e)&&(this.generation++,this._newMarker(!0,!1),await this._clear())}replaceAccessToken(e){if(!this.data||this.data.accessToken===e)return;const t=y(e);t.sid===y(this.data.accessToken).sid&&t.sub===this.data.user.userId&&(this.data={...this.data,accessToken:e,expiresIn:t.exp},this._publish("tokens",this._shareData()),this._reconnectClients(),this.schedule())}_cancelled(){const e=new Error("Authentication operation was superseded");return e.code="AUTH_OPERATION_CANCELLED",e}schedule(e){if(clearTimeout(this.timer),this.timer=null,!this.data||![...this.clients].some(e=>e._authConnectionRequested))return;const t=e??Math.max(1e3,1e3*Number(y(this.data.accessToken).exp||0)-this._serverNow()-(this.refreshMargin??6e4));this.timer=setTimeout(async()=>{try{const e=this.data?.accessToken;await this.getAccessToken({forceRefresh:!0,rejectedToken:e}),this.data&&await Promise.all([...this.clients].filter(e=>e._authConnectionRequested&&!e.isConnected()).map(e=>e.connectionManager.forceReconnect("access-token-renewed"))),this.schedule()}catch(e){f(e)||this.schedule(3e4)}},t),this.timer.unref?.()}destroy(){this.generation++,clearTimeout(this.timer),this.channel?.close(),globalThis.removeEventListener?.("storage",this.storageListener),this.clients.clear(),this.removeAllListeners(),S.get(this.serverUrl)===this&&S.delete(this.serverUrl)}}function I(e){return e&&"string"==typeof e?e.replace(/^Bearer\s+/i,""):""}function w(e){let t=e.replace(/-/g,"+").replace(/_/g,"/");const s=t.length%4;s&&(t+="=".repeat(4-s));const i=atob(t),n=Uint8Array.from(i,e=>e.charCodeAt(0));return(new TextDecoder).decode(n)}function T(e,t={}){const{bufferSeconds:s=30,validateExpiry:i=!0,validateIssuedAt:n=!1}=t;if(!e||"string"!=typeof e){const e=new Error("JWT token is required");throw e.code=r.JWT_INVALID,e}const a=I(e).split(".");if(3!==a.length){const e=new Error("Invalid JWT format: expected 3 parts separated by dots");throw e.code=r.JWT_INVALID,e}let o;try{o=JSON.parse(w(a[1]))}catch(e){const t=new Error("Invalid JWT: failed to decode payload");throw t.code=r.JWT_PARSE_FAILED,t.originalError=e,t}if(i&&o.exp){const e=1e3*o.exp,t=1e3*s,i=Date.now();if(e<i+t){const t=new Error(e<i?"JWT token expired":`JWT token expires within ${s} seconds`);throw t.code=r.JWT_EXPIRED,t.expiredAt=new Date(e).toISOString(),t}}if(n&&o.iat){const e=6e4;if(1e3*o.iat>Date.now()+e){const e=new Error("JWT token issued in the future");throw e.code=r.JWT_INVALID,e}}const c=o.userId||o.sub||o.user_id;if(!c){const e=new Error("Cannot extract userId from JWT: missing userId, sub, or user_id claim");throw e.code=r.JWT_INVALID,e}return{userId:c,payload:o}}function v(e){try{const t=I(e).split(".");if(3!==t.length)return null;const s=JSON.parse(w(t[1]));return s.userId||s.sub||s.user_id||null}catch(e){return console.error("Failed to extract userId from JWT:",e),null}}function A(e,t=0){try{const s=I(e).split(".");if(3!==s.length)return!0;const i=JSON.parse(w(s[1]));if(!i.exp)return!1;const n=1e3*i.exp,r=1e3*t;return n<Date.now()+r}catch(e){return console.error("Failed to check JWT expiration:",e),!0}}const R=()=>"undefined"!=typeof window&&window.SockJS?window.SockJS:s;class C extends i{constructor(e){super(),this.serverUrl=e.serverUrl.replace(/\/$/,""),this.jwtToken=e.jwtToken,this.apiKey=e.apiKey,this.projectId=e.projectId,this.useSockJS=!1!==e.useSockJS,this.reconnectDelay=e.reconnectDelay||g.reconnectDelay,this.maxReconnectDelay=e.maxReconnectDelay||g.maxReconnectDelay,this.maxReconnectAttempts=e.maxReconnectAttempts||g.maxReconnectAttempts,this.heartbeatIncoming=e.heartbeatIncoming||g.heartbeatIncoming,this.heartbeatOutgoing=e.heartbeatOutgoing||g.heartbeatOutgoing,this.resumeOnVisibility=!1!==e.resumeOnVisibility,this.logger=new m(e.logLevel||p.WARN,"ConnectionManager"),this.state=n.DISCONNECTED,this.stompClient=null,this.reconnectAttempts=0,this.subscriptions=new Map,this.pendingSubscriptions=[],this.userId=null,this._intentionalDisconnect=!1,this._tearingDown=!1,this._connectRequested=!1,this._forceReconnectPromise=null,this._lifecycleHandlers=null,this._installLifecycleListeners()}_installLifecycleListeners(){if("undefined"==typeof window||"function"!=typeof window.addEventListener)return;const e=()=>{"undefined"!=typeof document&&"visible"===document.visibilityState&&this._onResume("visibilitychange")},t=()=>this._onResume("pageshow"),s=()=>this._onResume("online");"undefined"!=typeof document&&"function"==typeof document.addEventListener&&document.addEventListener("visibilitychange",e),window.addEventListener("pageshow",t),window.addEventListener("online",s),this._lifecycleHandlers={onVisibilityChange:e,onPageShow:t,onOnline:s}}_removeLifecycleListeners(){if(!this._lifecycleHandlers)return;const{onVisibilityChange:e,onPageShow:t,onOnline:s}=this._lifecycleHandlers;"undefined"!=typeof document&&"function"==typeof document.removeEventListener&&document.removeEventListener("visibilitychange",e),"undefined"!=typeof window&&"function"==typeof window.removeEventListener&&(window.removeEventListener("pageshow",t),window.removeEventListener("online",s)),this._lifecycleHandlers=null}_onResume(e){if(this.resumeOnVisibility&&!this._intentionalDisconnect)if(this._connectRequested){if(this.state!==n.CONNECTING&&!this._forceReconnectPromise){if(this.state===n.CONNECTED)return this._isSocketAlive()?(this.logger.debug(`Resume (${e}): connection alive`),void this.emit("resumed",{reason:e})):(this.logger.warn(`Resume (${e}): zombie socket detected, forcing reconnect`),void this.forceReconnect(`zombie-socket:${e}`));this.logger.info(`Resume (${e}): reviving connection from state=${this.state}`),this.forceReconnect(e)}}else this.logger.debug(`Resume (${e}) ignored — connect() has not been called yet`)}_isSocketAlive(){const e=this.stompClient&&this.stompClient.webSocket;return!!e&&e.readyState===t.StompSocketState.OPEN}async forceReconnect(e){if(!this._intentionalDisconnect&&this.state!==n.CONNECTING)return this._forceReconnectPromise?(this.logger.debug(`Force reconnect already in progress, joining: ${e}`),this._forceReconnectPromise):(this._forceReconnectPromise=this._doForceReconnect(e).finally(()=>{this._forceReconnectPromise=null}),this._forceReconnectPromise)}async _doForceReconnect(e){if(this.logger.info(`Force reconnect: ${e}`),this.stompClient){this._tearingDown=!0;try{await this.stompClient.deactivate()}catch(e){this.logger.warn("Error deactivating stomp client on force reconnect:",e)}finally{this._tearingDown=!1}this.stompClient=null}this._setState(n.DISCONNECTED),this.reconnectAttempts=0;try{await this.connect()}catch(t){this.logger.warn(`Force reconnect failed (${e}):`,t)}}_setState(e){const t=this.state;this.state=e,this.emit("stateChange",{state:e,prevState:t}),this.logger.info(`State changed: ${t} -> ${e}`)}_getWebSocketUrl(){const e=this.useSockJS?l.SOCKJS_ENDPOINT:l.NATIVE_ENDPOINT;return`${this.serverUrl}${e}`}_createStompClient(){const e=this._getWebSocketUrl(),s={beforeConnect:async e=>{if(!this.getAccessToken)return;let t=0;for(;!this._intentionalDisconnect&&(!e||e.active);)try{const t=await this.getAccessToken({forceRefresh:!0===this._authRejected});if(this._intentionalDisconnect||e&&!e.active)return;return this.updateToken(t),void(this._authRejected=!1)}catch(s){if(this.emit("error",{type:r.CONNECTION_FAILED,message:s.message,error:s}),f(s))return this._setState(n.ERROR),this._connectReject?.(s),this._connectResolve=this._connectReject=null,void await((e||this.stompClient)?.deactivate());const i=Math.min(3e4,1e3*2**Math.min(t++,5))*(.8+.4*Math.random());await new Promise(e=>{this._cancelAuthRetry=()=>{clearTimeout(this._authRetryTimer),e()},this._authRetryTimer=setTimeout(e,i),this._authRetryTimer.unref?.()}),this._cancelAuthRetry=null}},connectHeaders:{Authorization:this.jwtToken.startsWith("Bearer ")?this.jwtToken:`Bearer ${this.jwtToken}`,"X-API-KEY":this.apiKey,"X-PROJECT-ID":this.projectId},heartbeatIncoming:this.heartbeatIncoming,heartbeatOutgoing:this.heartbeatOutgoing,reconnectDelay:this.reconnectDelay,reconnectTimeMode:t.ReconnectionTimeMode.EXPONENTIAL,maxReconnectDelay:this.maxReconnectDelay,debug:e=>{this.logger.level<=p.DEBUG&&this.logger.debug("STOMP:",e)},onConnect:e=>this._onConnect(e),onDisconnect:e=>this._onDisconnect(e),onStompError:e=>this._onStompError(e),onWebSocketClose:e=>this._onWebSocketClose(e),onWebSocketError:e=>this._onWebSocketError(e)};if(this.useSockJS){const t=R();s.webSocketFactory=()=>new t(e)}else{const e=this.serverUrl.startsWith("https")?"wss":"ws",t=this.serverUrl.replace(/^https?:\/\//,"");s.brokerURL=`${e}://${t}${l.NATIVE_ENDPOINT}`}return new("undefined"!=typeof window&&window.StompJs&&window.StompJs.Client?window.StompJs.Client:t.Client)(s)}async connect(){if(this.state!==n.CONNECTED){if(this.state!==n.CONNECTING)return this._setState(n.CONNECTING),this.reconnectAttempts=0,this._intentionalDisconnect=!1,this._connectRequested=!0,new Promise((e,t)=>{this._connectResolve=e,this._connectReject=t;try{this.stompClient=this._createStompClient(),this.stompClient.activate()}catch(e){this._setState(n.ERROR),this._drainPendingSubscriptions("Connection activation failed"),t(e)}});this.logger.warn("Connection in progress")}else this.logger.warn("Already connected")}_onConnect(e){this._authenticatedToken=this.jwtToken;const t=this.reconnectAttempts,s=t>0||!0===this._hasConnectedBefore;this._setState(n.CONNECTED),this.reconnectAttempts=0,this._hasConnectedBefore=!0,this.logger.info("Connected to server",e),this._restoreSubscriptions(),this._processPendingSubscriptions(),this.emit("connected",{frame:e}),s&&this.emit("reconnected",{attempts:t}),this._connectResolve&&(this._connectResolve(),this._connectResolve=null,this._connectReject=null)}_onDisconnect(e){this.logger.info("Disconnected from server",e),this._setState(n.DISCONNECTED),this.emit("disconnected",{frame:e})}_onStompError(e){try{const t=JSON.parse(e.body||e.headers?.message||"{}");this._authRejected=["ACCESS_UNAUTHORIZED","SESSION_REVOKED","TOKEN_INVALID"].includes(t.code)}catch{}this.logger.error("STOMP error:",e),this._setState(n.ERROR);const t={type:r.CONNECTION_FAILED,message:e.headers?.message||"STOMP error",frame:e};this._drainPendingSubscriptions(`STOMP error: ${t.message}`),this.emit("error",t),this._connectReject&&(this._connectReject(new Error(t.message)),this._connectResolve=null,this._connectReject=null)}_onWebSocketClose(e){this.stompClient&&1012===e?.code&&(this.stompClient.reconnectDelay=this.reconnectDelay*(.8+.4*Math.random())),4001===e?.code&&(this._authRejected=!0),this.logger.warn("WebSocket closed:",e),this._tearingDown?this.logger.debug("WebSocket closed during intentional teardown — not counting as reconnect"):this.state!==n.DISCONNECTED&&this._handleReconnect()}_onWebSocketError(e){this.logger.error("WebSocket error:",e),this._connectReject&&(this._drainPendingSubscriptions("WebSocket connection failed"),this._connectReject(new Error("WebSocket connection failed")),this._connectResolve=null,this._connectReject=null)}_handleReconnect(){if(this.reconnectAttempts++,this.reconnectAttempts>this.maxReconnectAttempts){if(this.stompClient){try{this.stompClient.deactivate()}catch(e){this.logger.warn("Error deactivating stomp client on max reconnect:",e)}this.stompClient=null}return this._drainPendingSubscriptions("Max reconnection attempts exceeded"),this._setState(n.ERROR),void this.emit("error",{type:r.CONNECTION_LOST,message:"Max reconnection attempts exceeded"})}this._setState(n.RECONNECTING),this.emit("reconnecting",{attempt:this.reconnectAttempts}),this.logger.info(`Reconnecting... attempt ${this.reconnectAttempts}`)}_restoreSubscriptions(){if(0===this.subscriptions.size)return;this.logger.info(`Restoring ${this.subscriptions.size} subscriptions after reconnect`);const e=new Map(this.subscriptions);this.subscriptions.clear(),e.forEach(({callback:e,headers:t},s)=>{this._subscribeInternal(s,e,t),this.logger.debug(`Restored subscription: ${s}`)})}_processPendingSubscriptions(){for(;this.pendingSubscriptions.length>0;){const{destination:e,callback:t,headers:s,resolve:i}=this.pendingSubscriptions.shift(),n=this._subscribeInternal(e,t,s);i&&i(n)}}_subscribeInternal(e,t,s={}){const i=this.stompClient.subscribe(e,e=>{try{const s=JSON.parse(e.body);t(s,e)}catch(s){this.logger.error("Failed to parse message:",s),t(e.body,e)}},s);return this.subscriptions.set(e,{subscription:i,callback:t,headers:s}),this.logger.debug(`Subscribed to: ${e}`),i}subscribe(e,t,s={}){return this.subscriptions.has(e)&&this.unsubscribe(e),this.state!==n.CONNECTED?new Promise((i,n)=>{this.pendingSubscriptions.push({destination:e,callback:t,headers:s,resolve:i,reject:n}),this.logger.debug(`Queued subscription for: ${e}`)}):Promise.resolve(this._subscribeInternal(e,t,s))}unsubscribe(e){const t=this.subscriptions.get(e);t&&(t.subscription.unsubscribe(),this.subscriptions.delete(e),this.logger.debug(`Unsubscribed from: ${e}`))}unsubscribeAll(){this.subscriptions.forEach(({subscription:e},t)=>{e.unsubscribe(),this.logger.debug(`Unsubscribed from: ${t}`)}),this.subscriptions.clear()}send(e,t,s={}){return this.state!==n.CONNECTED?(this.logger.warn("Cannot send message: not connected"),!1):(this.stompClient.publish({destination:e,body:JSON.stringify(t),headers:s}),this.logger.debug(`Sent to ${e}:`,t),!0)}updateToken(e){this.jwtToken=e,this.stompClient&&(this.stompClient.connectHeaders={...this.stompClient.connectHeaders,Authorization:e.startsWith("Bearer ")?e:`Bearer ${e}`})}async disconnect(){this._intentionalDisconnect=!0,this._connectRequested=!1,this._cancelAuthRetry?.(),this._connectReject?.(Object.assign(new Error("Connection cancelled"),{code:"AUTH_OPERATION_CANCELLED"})),this._connectResolve=this._connectReject=null,this._drainPendingSubscriptions("Disconnected before subscription completed"),this.stompClient&&(this.unsubscribeAll(),await this.stompClient.deactivate(),this.stompClient=null),this._setState(n.DISCONNECTED),this.logger.info("Disconnected")}_drainPendingSubscriptions(e){if(0===this.pendingSubscriptions.length)return;const t=this.pendingSubscriptions;this.pendingSubscriptions=[],t.forEach(({destination:t,reject:s})=>{s&&s(new Error(`${e}: ${t}`))}),this.logger.debug(`Drained ${t.length} pending subscriptions: ${e}`)}isConnected(){return this.state===n.CONNECTED}getState(){return this.state}setLogLevel(e){this.logger.setLevel(e)}async destroy(){this._removeLifecycleListeners(),await this.disconnect(),this.removeAllListeners(),this.logger.info("ConnectionManager destroyed")}}const M=new Set(["text/plain","text/markdown","text/x-markdown"]);class k extends i{constructor(e){super(),this.connectionManager=e.connectionManager,this.apiClient=e.apiClient,this.userId=e.userId,this.logger=new m(e.logLevel||p.WARN,"ChatClient"),this.subscribedRooms=new Map,this.roomListSubscribed=!1,this._activeRoomId=null,this._typingTimers=new Map,this._assistantTypingTimers=new Map,this._assistantTypingTimeoutMs=3e4,this._assistantTypingUserId="__assistant__",this._assistantTypingUserName="AI",this._assistantProgress=new Map,this._assistantStreamTombstones=new Map,this._assistantTombstoneTtlMs=12e4,this._assistantTombstoneWarnThreshold=32,this._assistantTombstoneWarned=new Set,this._assistantSlotSeq=0,this._assistantRoomGeneration=new Map,this._assistantSnapshotPending=new Map,this._assistantSnapshotInFlight=new Map,this.assistantStreamReplay=!1!==e.assistantStreamReplay,this._assistantPhaseLabels={THINKING:"생각 중…",SEARCHING:"검색 중…",PLANNING:"기획 중…",DOCUMENT_ANALYSIS:"문서 분석 중…",WRITING:"작성 중…"},this._assistantProgressFallbackTtlMs=35e3,this._seenChatMessageIdsByRoom=new Map,this._seenRoomListMessageIdsByRoom=new Map,this.syncOnReconnect=!1!==e.syncOnReconnect,this.rewindMillis=void 0!==e.rewindMillis?e.rewindMillis:g.rewindMillis,this.maxSyncPages=e.maxSyncPages||g.maxSyncPages,this.syncPageSize=e.syncPageSize||g.syncPageSize,this._anchor=new Map,this._anchorGen=new Map,this._syncInFlight=new Set,this._replaying=!1,this._maxSeenPerRoom=Math.max(200,this.maxSyncPages*this.syncPageSize+100),this._installSyncTriggers()}_installSyncTriggers(){this.connectionManager&&"function"==typeof this.connectionManager.on&&(this._onReconnectedForSync=({attempts:e}={})=>{this.syncAfterReconnect(`reconnected:${e??0}`).catch(e=>this.logger.warn("Gap sync after reconnect failed:",e)),this.resumeAssistantStreams(`reconnected:${e??0}`).catch(e=>this.logger.warn("Assistant stream replay after reconnect failed:",e))},this._onResumedForSync=({reason:e}={})=>{this.syncAfterReconnect(`resumed:${e??"unknown"}`).catch(e=>this.logger.warn("Gap sync after resume failed:",e)),this.resumeAssistantStreams(`resumed:${e??"unknown"}`).catch(e=>this.logger.warn("Assistant stream replay after resume failed:",e))},this.connectionManager.on("reconnected",this._onReconnectedForSync),this.connectionManager.on("resumed",this._onResumedForSync))}_shouldDedupMessage(e,t,s){if(!t||!s)return!1;let i=e.get(t);if(i||(i=new Map,e.set(t,i)),i.has(s))return!0;if(i.set(s,!0),i.size>this._maxSeenPerRoom){const e=i.keys().next().value;i.delete(e)}return!1}async getRooms(e={}){const{size:t=50,lastId:s,lastSortValue:i,type:n}=e;return this.apiClient.get("/api/v1/rooms/my",{size:t,lastId:s,lastSortValue:i,type:n})}async getRoom(e){const t=this._anchorGen.get(e)||0,s=await this.apiClient.get(`/api/v1/rooms/${e}`),i=this._unwrapSuccessResponse(s);return i&&Array.isArray(i.messages)&&this._seedAnchorFromLatest(e,i.messages,t),s}async enterRoom(e){const t=this.subscribedRooms.has(e);if(t){if(this.assistantStreamReplay){const t=this._armAssistantSnapshot(e);this._loadAssistantSnapshot(e,t).catch(t=>this.logger.warn(`Assistant snapshot on re-enter failed: room=${e}`,t))}}else await this.subscribeRoom(e);this.setActiveRoom(e);try{return await this.getRoom(e)}catch(s){throw this.getActiveRoom()===e&&this.clearActiveRoom(),t||this.unsubscribeRoom(e),s}}async getRoomInfo(e){return this.apiClient.get(`/api/v1/rooms/${e}/info`)}async setMyRoomLanguage(e,t){return this.apiClient.put(`/api/v1/rooms/${e}/my-language`,{language:t})}async createOneToOneRoom(e){return this.apiClient.post(`/api/v1/rooms/direct/${e}`)}async createGroupRoom(e){const t=e.roomType||o.GROUP;if(t===o.PRIVATE_GROUP){if(!e.password||e.password.length<4)throw new Error("PRIVATE_GROUP requires a password of at least 4 characters")}else if(t===o.GROUP){if(e.password)throw new Error("Public GROUP rooms cannot have a password")}else if(t===o.TEAM&&e.password)throw new Error("TEAM rooms cannot have a password (invite-only)");return this.apiClient.post("/api/v1/rooms/group",{roomName:e.roomName,description:e.description,invitedUserIds:e.invitedUserIds,roomType:t,password:e.password,messageRetentionHours:e.messageRetentionHours,invitedAssistantPersonaIds:e.invitedAssistantPersonaIds,assistantMode:e.assistantMode,roomAiType:e.roomAiType,engagementIntensity:e.engagementIntensity})}async getAssistants(){const e=await this.apiClient.get("/api/v1/assistants");return this._unwrapSuccessResponse(e)}async getRoomAiMeta(){const e=await this.apiClient.get("/api/v1/rooms/ai-meta");return this._unwrapSuccessResponse(e)}async rateAssistantMessage(e,t,s,i){if(!Number.isInteger(s)||s<1||s>5)throw new Error("rating must be an integer between 1 and 5");await this.apiClient.post(`/api/v1/chat-messages/${e}/messages/${t}/rating`,{rating:s,comment:i})}async summarizeWithAssistant(e,t={}){const{personaId:s,format:i,messageCount:n}=t;if(!s)throw new Error("summarizeWithAssistant requires options.personaId");const r=await this.apiClient.post(`/api/v1/rooms/${e}/assistant/tools/summarize`,{personaId:s,format:i,messageCount:n});return this._unwrapSuccessResponse(r)}async translateWithAssistant(e,t={}){const{targetLang:s,sourceMessageId:i}=t;if(!i)throw new Error("translateWithAssistant requires options.sourceMessageId");if(!s)throw new Error("translateWithAssistant requires options.targetLang");const n=await this.apiClient.post(`/api/v1/rooms/${e}/assistant/tools/translate`,{targetLang:s,sourceMessageId:i});return this._unwrapSuccessResponse(n)}async refreshSummary(e,t={}){const{format:s,lang:i}=t,n=await this.apiClient.post(`/api/v1/rooms/${e}/summary/refresh`,{format:s,lang:i});return this._unwrapSuccessResponse(n)}async getSummary(e,t={}){const{format:s,lang:i,afterSeq:n,size:r}=t,a=await this.apiClient.get(`/api/v1/rooms/${e}/summary`,{format:s,lang:i,afterSeq:n,size:r});return this._unwrapSuccessResponse(a)}async getRoomPmPrompt(e){const t=await this.apiClient.get(`/api/v1/rooms/${e}/pm-prompt`);return this._unwrapSuccessResponse(t)}async upsertRoomPmPrompt(e,t){if("string"!=typeof t||!t.trim())throw new Error("upsertRoomPmPrompt requires non-blank content");if(t.length>2e4)throw new Error("upsertRoomPmPrompt content exceeds 20000 characters");const s=await this.apiClient.put(`/api/v1/rooms/${e}/pm-prompt`,{content:t});return this._unwrapSuccessResponse(s)}async activateRoomPmPrompt(e){const t=await this.apiClient.patch(`/api/v1/rooms/${e}/pm-prompt/activate`);return this._unwrapSuccessResponse(t)}async deactivateRoomPmPrompt(e){const t=await this.apiClient.patch(`/api/v1/rooms/${e}/pm-prompt/deactivate`);return this._unwrapSuccessResponse(t)}async getRoomPmPromptVersions(e){const t=await this.apiClient.get(`/api/v1/rooms/${e}/pm-prompt/versions`);return this._unwrapSuccessResponse(t)}async activateRoomPmPromptVersion(e,t){if(!Number.isInteger(t)||t<1)throw new Error("activateRoomPmPromptVersion requires a positive integer version");const s=await this.apiClient.post(`/api/v1/rooms/${e}/pm-prompt/versions/${t}/activate`);return this._unwrapSuccessResponse(s)}async previewRoomPmPrompt(e){const t=await this.apiClient.get(`/api/v1/rooms/${e}/pm-prompt/preview`);return this._unwrapSuccessResponse(t)}async joinGroupRoom(e,t){const s=t?{password:t}:void 0;return this.apiClient.post(`/api/v1/rooms/group/${e}/join`,s)}async leaveRoom(e){const t=await this.apiClient.post(`/api/v1/rooms/${e}/leave`);return this.unsubscribeRoom(e),t}async updateGroupRoom(e,t){return this.apiClient.put(`/api/v1/rooms/group/${e}`,t)}async inviteToGroupRoom(e,t){const s=Array.isArray(t)?{userIds:t}:{userIds:t?.userIds,assistantPersonaIds:t?.assistantPersonaIds};return this.apiClient.post(`/api/v1/rooms/group/${e}/invite`,s)}async kickMember(e,t){return this.apiClient.delete(`/api/v1/rooms/${e}/members/${t}`)}async banMember(e,t){return this.apiClient.post(`/api/v1/rooms/${e}/banned-members`,{userId:t})}async unbanMember(e,t){return this.apiClient.delete(`/api/v1/rooms/${e}/banned-members/${t}`)}async getBannedMembers(e){const t=await this.apiClient.get(`/api/v1/rooms/${e}/banned-members`);return t&&"object"==typeof t&&"data"in t?t.data:t}async pinMessage(e,t){return this.apiClient.post(`/api/v1/rooms/group/${e}/pin/${t}`)}async unpinMessage(e){return this.apiClient.post(`/api/v1/rooms/group/${e}/unpin`)}async createVote(e,t,s={}){if(!e)throw new Error("roomId is required");const{idempotencyKey:i}=s;if(!i)throw new Error("createVote: options.idempotencyKey is required — generate it once per vote and reuse the same value on retry (재시도에 같은 키를 넘겨야 투표가 중복 생성되지 않는다).");const n=await this.apiClient.post(`/api/v1/rooms/${e}/votes`,t,void 0,{"X-Idempotency-Key":i});return this._unwrapSuccessResponse(n)}async getVotes(e,t={}){const{cursor:s,limit:i}=t,n=await this.apiClient.get(`/api/v1/rooms/${e}/votes`,{cursor:s,limit:i});return this._unwrapSuccessResponse(n)}async getVote(e,t){const s=await this.apiClient.get(`/api/v1/rooms/${e}/votes/${t}`);return this._unwrapSuccessResponse(s)}async updateVote(e,t,s){const i=await this.apiClient.patch(`/api/v1/rooms/${e}/votes/${t}`,s);return this._unwrapSuccessResponse(i)}async deleteVote(e,t){await this.apiClient.delete(`/api/v1/rooms/${e}/votes/${t}`)}async closeVote(e,t){await this.apiClient.patch(`/api/v1/rooms/${e}/votes/${t}/close`)}async submitVote(e,t,s){const i=await this.apiClient.put(`/api/v1/rooms/${e}/votes/${t}/responses`,{optionIds:s});return this._unwrapSuccessResponse(i)}async uploadVoteImage(e,t,s={}){if(!e)throw new Error("roomId is required");if(!t)throw new Error("file is required");const i=await this.apiClient.upload(`/api/v1/rooms/${e}/votes/images`,t,{onProgress:s.onProgress,signal:s.signal});return this._unwrapSuccessResponse(i)}async toggleReaction(e,t,s){return this.apiClient.post(`/api/v1/chat-messages/${e}/messages/${t}/reaction`,{emoji:s})}async getAvailableGroupRooms(e={}){const{size:t=50,lastId:s,lastSortValue:i}=e;return this.apiClient.get("/api/v1/rooms/groups/available",{size:t,lastId:s,lastSortValue:i})}async getAllGroupRooms(e={}){const{size:t=50,lastId:s,lastSortValue:i}=e;return this.apiClient.get("/api/v1/rooms/groups/all",{size:t,lastId:s,lastSortValue:i})}async getMessages(e,t={}){const{size:s=50,lastId:i,lastSortValue:n}=t,r=null==i&&null==n,a=this._anchorGen.get(e)||0,o=await this.apiClient.get(`/api/v1/chat-messages/${e}/list`,{size:s,lastId:i,lastSortValue:n}),c=this._unwrapSuccessResponse(o);return c&&Array.isArray(c.content)&&(c.content.forEach(e=>this._normalizeMessageTimestamps(e)),r&&this._seedAnchorFromLatest(e,c.content,a)),o}_seedAnchorFromLatest(e,t,s){if(!Array.isArray(t)||0===t.length)return!1;const i=t[0];return!(!i||!i.id)&&this._commitAnchorFromSync(e,i.id,s)}async syncMessages(e,t={}){const{afterId:s,rewindMillis:i,size:n=this.syncPageSize}=t,r=await this.apiClient.get(`/api/v1/chat-messages/${e}/sync`,{afterId:s,rewindMillis:i,size:n}),a=this._unwrapSuccessResponse(r);return a&&Array.isArray(a.content)&&a.content.forEach(e=>this._normalizeMessageTimestamps(e)),r}_advanceAnchorFromLive(e,t){e&&t&&(this._anchor.set(e,t),this._anchorGen.set(e,(this._anchorGen.get(e)||0)+1))}_commitAnchorFromSync(e,t,s){return!!t&&((this._anchorGen.get(e)||0)!==s?(this.logger.debug(`Anchor commit skipped (live advanced): room=${e}`),!1):(this._anchor.set(e,t),!0))}_replayMessages(e,t){if(!Array.isArray(t)||0===t.length)return 0;let s=0;this._replaying=!0;try{t.forEach(t=>{this._handleChatMessage(e,{...t,eventType:t.eventType||"MESSAGE_CREATED"})&&(s+=1)})}finally{this._replaying=!1}return s}async _reloadLatestPage(e,t){const s=await this.getMessages(e,{size:this.syncPageSize}),i=this._unwrapSuccessResponse(s)||{},n=(Array.isArray(i.content)?i.content:[]).slice().reverse(),r=this._replayMessages(e,n),a=n.length?n[n.length-1].id:null;return this._commitAnchorFromSync(e,a,t),r}_isCursorExpired(e){return 410===e?.status||"SYNC_CURSOR_EXPIRED"===e?.response?.code}async _syncRoom(e){const t=this._anchor.get(e);if(!t)return null;if(this._syncInFlight.has(e))return null;this._syncInFlight.add(e);const s=this._anchorGen.get(e)||0,i=Date.now();try{const n=[];let r=t,a=this.rewindMillis,o=!0,c=0;for(;o&&c<this.maxSyncPages;){const t=await this.syncMessages(e,{afterId:r,rewindMillis:a}),s=this._unwrapSuccessResponse(t)||{},i=Array.isArray(s.content)?s.content:[];n.push(...i),a=0,o=!!s.hasNext,s.lastId&&(r=s.lastId),c+=1}if(o){this.logger.info(`Gap too large, falling back to latest page: room=${e}`),this.emit("gapTruncated",{roomId:e,fetched:n.length});return{roomId:e,recovered:await this._reloadLatestPage(e,s),truncated:!0,durationMs:Date.now()-i}}const l=this._replayMessages(e,n);return this._commitAnchorFromSync(e,r,s),{roomId:e,recovered:l,truncated:!1,durationMs:Date.now()-i}}catch(t){if(this._isCursorExpired(t)){this.logger.warn(`Sync cursor expired, reloading latest: room=${e}`),this.emit("gapTruncated",{roomId:e,fetched:0,reason:"cursorExpired"});return{roomId:e,recovered:await this._reloadLatestPage(e,s),truncated:!0,durationMs:Date.now()-i}}throw t}finally{this._syncInFlight.delete(e)}}async syncAfterReconnect(e="manual"){if(!this.syncOnReconnect)return;const t=Array.from(this.subscribedRooms.keys()).filter(e=>this._anchor.has(e));if(0===t.length)return void await this._refreshRoomList(e);this.emit("syncStarted",{roomIds:t,reason:e});const s=Date.now(),i=[];for(const e of t)try{const t=await this._syncRoom(e);t&&i.push(t)}catch(t){this.logger.warn(`Gap sync failed: room=${e}`,t)}await this._refreshRoomList(e),this.emit("syncCompleted",{rooms:i,totalRecovered:i.reduce((e,t)=>e+(t.recovered||0),0),durationMs:Date.now()-s,reason:e})}async _refreshRoomList(e){if(this.roomListSubscribed)try{const t=await this.getRooms({size:50}),s=this._unwrapSuccessResponse(t)||{};this.emit("roomListSynced",{rooms:s.content||[],reason:e})}catch(e){this.logger.warn("Room list refresh failed (non-fatal):",e)}}async fetchLinkPreview(e){const t="string"==typeof e?e.trim():"";if(!t)throw new Error("url is required");const s=await this.apiClient.post("/api/v1/link-preview",{url:t});return this._unwrapSuccessResponse(s)}async sendMessage(e,t){const s=this._generateMessageId();return this._sendMessageWithId(e,s,t)}sendMessageOptimistic(e,t){const s=this._generateMessageId(),i=[s],n=this._sendMessageWithId(e,s,t).then(e=>this._attachOptimisticMetadata(i,e));return{baseMessageId:s,messageIds:i,promise:n}}async _sendMessageWithId(e,t,s){this.stopTyping(e);const i=await this.apiClient.post(`/api/v1/chat-messages/${e}/send`,{messageId:t,message:s.message,fileInfos:s.fileInfos,replyToMessageId:s.replyToMessageId,decisionResponse:s.decisionResponse}),n=this._unwrapSuccessResponse(i);return n&&Array.isArray(n.messages)&&n.messages.forEach(e=>this._normalizeMessageTimestamps(e)),n}async sendTextMessage(e,t){return this.sendMessage(e,{message:t})}sendTextMessageOptimistic(e,t){return this.sendMessageOptimistic(e,{message:t})}async uploadFile(e,t,s={}){if(!e)throw new Error("roomId is required");if(!t)throw new Error("file is required");const i=await this.apiClient.upload(`/api/v1/files/${e}/upload`,t,{onProgress:s.onProgress,signal:s.signal});return this._unwrapSuccessResponse(i)}async sendFileMessage(e,t,s={}){const i=this._normalizeFileArray(t),n=await this._uploadFiles(e,i,s),r=this._applyFileMetadata(n,s.metadata);return this.sendMessage(e,{message:s.message,fileInfos:r,replyToMessageId:s.replyToMessageId})}sendFileMessageOptimistic(e,t,s={}){const i=this._normalizeFileArray(t),n=this._generateMessageId(),r=[n],a=(async()=>{const t=await this._uploadFiles(e,i,s),a=this._applyFileMetadata(t,s.metadata),o=await this._sendMessageWithId(e,n,{message:s.message,fileInfos:a,replyToMessageId:s.replyToMessageId});return this._attachOptimisticMetadata(r,o)})();return{baseMessageId:n,messageIds:r,promise:a}}_normalizeFileArray(e){const t=Array.isArray(e)?e:[e];if(0===t.length)throw new Error("At least one file is required");if(t.length>20)throw new Error("A maximum of 20 files can be sent in one message");return t}async _uploadFiles(e,t,s={}){return Promise.all(t.map((t,i)=>this.uploadFile(e,t,{signal:s.signal,onProgress:s.onUploadProgress?e=>s.onUploadProgress({...e,fileIndex:i}):void 0})))}_applyFileMetadata(e,t){return null==t?e:e.map((e,s)=>{const i=Array.isArray(t)?t[s]:t;return null==i?e:{...e,metadata:i}})}async sendReply(e,t,s){return this.sendMessage(e,{message:t,replyToMessageId:s})}sendReplyOptimistic(e,t,s){return this.sendMessageOptimistic(e,{message:t,replyToMessageId:s})}async sendDecisionChoice(e,t,s){if(!t||!t.messageId)throw new Error("sendDecisionChoice: card with messageId is required");if(!s||!s.id)throw new Error("sendDecisionChoice: option with id is required");return this.sendMessage(e,{message:s.label,replyToMessageId:t.messageId,decisionResponse:{optionId:s.id}})}async deleteMessage(e,t,s="ALL"){return this.apiClient.post(`/api/v1/chat-messages/${e}/messages/${t}/delete`,{deleteType:s})}async editMessage(e,t,s){return this.apiClient.put(`/api/v1/chat-messages/${e}/messages/${t}`,{message:s})}_hasTextMessage(e){return"string"==typeof e&&e.trim().length>0}_baseMimeType(e){const t=e||"",s=t.indexOf(";");return(s>=0?t.slice(0,s):t).trim().toLowerCase()}classifyFileType(e){const t=this._baseMimeType(e);return t.startsWith("image/")?"IMAGE":t.startsWith("video/")?"VIDEO":t.startsWith("audio/")?"AUDIO":t.includes("pdf")||t.includes("document")||t.includes("msword")||t.includes("ms-excel")||t.includes("ms-powerpoint")||M.has(t)?"DOCUMENT":"FILE"}_classifyFileType(e){return this.classifyFileType(e)}resolveMessageCategory(e){const t=e&&e.chatMessageType,s=e&&e.fileMetaDataList||[];if("GALLERY"!==t)return t||"TEXT";if(0===s.length)return"FILE";const i=new Set(s.map(e=>this.classifyFileType(e&&e.fileType||"")));return 1===i.size?i.values().next().value:"MIXED"}_attachOptimisticMetadata(e,t){const s=t&&Array.isArray(t.messages)?t.messages.map(e=>e&&e.messageId||null):[],i=s.length===e.length&&e.every((e,t)=>e===s[t]);return i||this.logger.warn("Optimistic messageId prediction mismatch — server may have changed grouping rules. Use result.optimistic.actualMessageIds to reconcile.",{predicted:e,actual:s}),{...t,optimistic:{predictedMessageIds:e,actualMessageIds:s,predictionMatched:i}}}_generateMessageId(){return"undefined"!=typeof crypto&&"function"==typeof crypto.randomUUID?crypto.randomUUID():"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,e=>{const t=16*Math.random()|0;return("x"===e?t:3&t|8).toString(16)})}_unwrapSuccessResponse(e){return e&&"object"==typeof e&&Object.prototype.hasOwnProperty.call(e,"data")?e.data:e}_normalizeMessageTimestamps(e){return e&&"object"==typeof e?(e.sentAt=this._toEpochMillis(e.sentAt),e.editedAt=this._toEpochMillis(e.editedAt),e.replyTo&&"object"==typeof e.replyTo&&(e.replyTo.sentAt=this._toEpochMillis(e.replyTo.sentAt)),e):e}_toEpochMillis(e){if("string"!=typeof e)return e;const t=Date.parse(e);return Number.isNaN(t)?e:t}async subscribeRoom(e){if(this.subscribedRooms.has(e))return void this.logger.warn(`Already subscribed to room: ${e}`);const t=l.getChatDestination(e);await this.connectionManager.subscribe(t,t=>{this._handleChatMessage(e,t)});const s=l.getChatReadDestination(e);await this.connectionManager.subscribe(s,t=>{this._handleReadEvent(e,t)});const i=l.getChatTypingDestination(e);await this.connectionManager.subscribe(i,t=>{this._handleTypingEvent(e,t)});const n=this.assistantStreamReplay?this._armAssistantSnapshot(e):null,r=l.getChatAssistantStreamDestination(e);await this.connectionManager.subscribe(r,t=>{this._handleAssistantStreamEvent(e,t)}),null!==n&&this._loadAssistantSnapshot(e,n).catch(t=>this.logger.warn(`Assistant snapshot merge failed: room=${e}`,t));const a=l.getChatSummaryDestination(e);await this.connectionManager.subscribe(a,t=>{this._handleSummaryEvent(e,t)});const o=t=>{t.roomId===e&&this.emit("memberJoined",{roomId:e,members:t.members||[],participantCount:t.activeParticipantCount,timestamp:t.timestamp})},c=t=>{t.roomId===e&&this.emit("memberLeft",{roomId:e,members:t.members||[],participantCount:t.activeParticipantCount,timestamp:t.timestamp})};this.on("roomListJoined",o),this.on("roomListLeft",c),this.subscribedRooms.set(e,{chatDestination:t,readDestination:s,typingDestination:i,assistantStreamDestination:r,summaryDestination:a,memberJoinedHandler:o,memberLeftHandler:c,subscribedAt:new Date}),this.logger.info(`Subscribed to room: ${e}`),this.emit("roomSubscribed",{roomId:e})}unsubscribeRoom(e){const t=this.subscribedRooms.get(e);t?(this.connectionManager.unsubscribe(t.chatDestination),this.connectionManager.unsubscribe(t.readDestination),t.typingDestination&&this.connectionManager.unsubscribe(t.typingDestination),t.assistantStreamDestination&&this.connectionManager.unsubscribe(t.assistantStreamDestination),t.summaryDestination&&this.connectionManager.unsubscribe(t.summaryDestination),t.memberJoinedHandler&&this.off("roomListJoined",t.memberJoinedHandler),t.memberLeftHandler&&this.off("roomListLeft",t.memberLeftHandler),this._clearTypingTimer(e),this._clearAssistantTypingTimer(e),this._clearAssistantProgress(e),this._activeRoomId===e&&(this._activeRoomId=null),this.subscribedRooms.delete(e),this._seenChatMessageIdsByRoom.delete(e),this._seenRoomListMessageIdsByRoom.delete(e),this.logger.info(`Unsubscribed from room: ${e}`),this.emit("roomUnsubscribed",{roomId:e})):this.logger.warn(`Not subscribed to room: ${e}`)}unsubscribeAllRooms(){this.subscribedRooms.forEach((e,t)=>{this.unsubscribeRoom(t)})}setActiveRoom(e){this._activeRoomId=e,this.logger.debug(`Active room set: ${e}`)}clearActiveRoom(){this.logger.debug(`Active room cleared (was: ${this._activeRoomId})`),this._activeRoomId=null}getActiveRoom(){return this._activeRoomId}async subscribeRoomList(){if(this.roomListSubscribed)return void this.logger.warn("Already subscribed to room list");const e=l.ROOM_LIST_USER_DESTINATION;await this.connectionManager.subscribe(e,e=>{this._handleRoomListEvent(e)}),this.roomListSubscribed=!0,this.logger.info("Subscribed to room list"),this.emit("roomListSubscribed",{})}unsubscribeRoomList(){this.roomListSubscribed?(this.connectionManager.unsubscribe(l.ROOM_LIST_USER_DESTINATION),this.roomListSubscribed=!1,this.logger.info("Unsubscribed from room list"),this.emit("roomListUnsubscribed",{})):this.logger.warn("Not subscribed to room list")}isRoomListSubscribed(){return this.roomListSubscribed}markAsRead(e,t){return this.connectionManager.isConnected()?this.connectionManager.send(l.CHAT_READ,{roomId:e,messageId:t}):(this.logger.warn("Cannot mark as read: not connected"),!1)}_handleChatMessage(e,t){if(this.logger.debug(`Message received in room ${e} (${t.eventType}):`,t),"MESSAGE_CREATED"===t.eventType&&this._shouldDedupMessage(this._seenChatMessageIdsByRoom,e,t.messageId))return this.logger.debug(`Duplicate MESSAGE_CREATED suppressed: room=${e}, messageId=${t.messageId}`),!1;switch("MESSAGE_CREATED"!==t.eventType||this._replaying||this._advanceAnchorFromLive(e,t.id),"MESSAGE_UPDATED"===t.eventType&&(void 0===t.translations&&(t.translations=null),void 0===t.sourceLang&&(t.sourceLang=null),void 0===t.translationsOf&&(t.translationsOf=null)),this.emit("message",{roomId:e,message:t}),t.eventType){case"MESSAGE_CREATED":"ASSISTANT"===t.senderType&&(this._clearAssistantTypingTimer(e),this.emit("typing",{roomId:e,userId:this._assistantTypingUserId,userName:this._assistantTypingUserName,typing:!1,senderType:"ASSISTANT"})),t.userId!==this.userId&&(this.emit("newMessage",{roomId:e,message:t}),this._activeRoomId===e&&t.messageId&&this.markAsRead(e,t.messageId));break;case"MESSAGE_UPDATED":this.emit("messageUpdated",{roomId:e,message:t});break;case"MESSAGE_DELETED":this.emit("messageDeleted",{roomId:e,message:t});break;case"REACTION_CHANGED":this.emit("reactionChanged",{roomId:e,message:t});break;case"LINK_PREVIEW_ATTACHED":this.emit("linkPreviewAttached",{roomId:e,message:t});break;case"MESSAGE_TRANSLATED":this.emit("messageTranslated",{roomId:e,message:t});break;default:this.logger.warn(`Unknown eventType "${t.eventType}" — falling back to legacy behavior`),t.userId!==this.userId&&(this.emit("newMessage",{roomId:e,message:t}),this._activeRoomId===e&&t.messageId&&this.markAsRead(e,t.messageId))}return!0}_handleRoomListEvent(e){if(this.logger.debug("Room list event:",e),e.eventType===c.MESSAGE_RECEIVED&&e.messageId&&this._shouldDedupMessage(this._seenRoomListMessageIdsByRoom,e.roomId,e.messageId))this.logger.debug(`Duplicate roomList MESSAGE_RECEIVED suppressed: room=${e.roomId}, messageId=${e.messageId}`);else switch(this.emit("roomListUpdate",e),e.eventType){case c.MESSAGE_RECEIVED:this.emit("roomListMessage",e);break;case c.MESSAGE_DELETED:this.emit("roomListMessageDeleted",e);break;case c.MESSAGE_UPDATED:this.emit("roomListMessageUpdated",e);break;case c.ROOM_CREATED:this.emit("roomListCreated",e);break;case c.ROOM_JOINED:this.emit("roomListJoined",e);break;case c.ROOM_LEFT:this.emit("roomListLeft",e),e.actorId===this.userId&&this.emit("roomListSelfLeft",e);break;case c.ROOM_KICKED:this.emit("roomListKicked",e),this._isSelfInMembers(e)&&this.emit("roomListSelfKicked",e);break;case c.ROOM_BANNED:this.emit("roomListBanned",e),this._isSelfInMembers(e)&&this.emit("roomListSelfBanned",e);break;case c.ROOM_UPDATED:this.emit("roomListRoomUpdated",e);break;case c.MESSAGE_RETENTION_CLEANUP:this.emit("retentionCleanup",{roomId:e.roomId,cutoffTime:e.cutoffTime});break;case c.ROOM_UNREAD_UPDATED:this.emit("roomListUnreadUpdated",{roomId:e.roomId,roomUnreadCount:e.roomUnreadCount,unreadVersion:e.unreadVersion});break;default:this.logger.warn("Unknown room list event type:",e.eventType)}}_handleReadEvent(e,t){this.logger.debug(`Read event in room ${e}:`,t);const s=t.roomId||e;t.events&&Array.isArray(t.events)?t.events.forEach(e=>{this.emit("messageRead",{roomId:s,messageId:e.messageId,userId:t.userId,remainingUnreadCount:e.remainingUnreadCount})}):this.emit("messageRead",{roomId:s,messageId:t.messageId,userId:t.userId,remainingUnreadCount:t.remainingUnreadCount})}startTyping(e){if(!this.connectionManager.isConnected())return;const t=this._typingTimers.get(e);t?clearTimeout(t):this.connectionManager.send(l.CHAT_TYPING,{roomId:e,typing:!0});const s=setTimeout(()=>{this.stopTyping(e)},3e3);this._typingTimers.set(e,s)}stopTyping(e){this._clearTypingTimer(e),this.connectionManager.isConnected()&&this.connectionManager.send(l.CHAT_TYPING,{roomId:e,typing:!1})}_isSelfInMembers(e){return Array.isArray(e.members)&&e.members.some(e=>e&&e.userId===this.userId)}_handleTypingEvent(e,t){"ASSISTANT"!==t.senderType&&t.userId===this.userId||("ASSISTANT"===t.senderType&&!0===t.typing&&this._startAssistantTypingTimer(e),this.emit("typing",{roomId:e,userId:t.userId,userName:t.userName,typing:t.typing,senderType:t.senderType||"USER"}))}_startAssistantTypingTimer(e){this._clearAssistantTypingTimer(e);const t=setTimeout(()=>{this._assistantTypingTimers.delete(e),this.emit("typing",{roomId:e,userId:this._assistantTypingUserId,userName:this._assistantTypingUserName,typing:!1,senderType:"ASSISTANT"}),this.logger.debug(`Assistant typing auto-cleared (timeout): room=${e}`)},this._assistantTypingTimeoutMs);this._assistantTypingTimers.set(e,t)}_clearAssistantTypingTimer(e){const t=this._assistantTypingTimers.get(e);t&&(clearTimeout(t),this._assistantTypingTimers.delete(e))}_clearTypingTimer(e){const t=this._typingTimers.get(e);t&&(clearTimeout(t),this._typingTimers.delete(e))}_handleAssistantStreamEvent(e,t){if(!t||!t.type)return;const s=t.streamId||null;if(!s)return void this.logger.debug(`Assistant stream event without streamId ignored: room=${e}, type=${t.type}`);if(this._isAssistantStreamClosed(e,s))return void this.logger.debug(`Assistant event for closed stream ignored: room=${e}, stream=${s}, type=${t.type}`);const i=this._assistantSnapshotPending.get(e);if(i&&"DONE"!==t.type)i.events.push(t);else{if("PHASE"===t.type){const i=this._assistantSlot(e,s),n="number"==typeof t.createdAt?t.createdAt:null;if(i&&null!==n&&n<=(i.phaseUpdatedAt||0))return void this.logger.debug(`Stale assistant PHASE ignored: room=${e}, stream=${s}, createdAt=${n}, applied=${i.phaseUpdatedAt}`);const r=t.phase||null,a=Boolean(i&&i.phase===r),o=t.progress?this._normalizeAssistantProgress(t.progress):a&&i?i.progress:null,c=i,l=this._setAssistantSlot(e,s,t,{phase:r,progress:o,phaseUpdatedAt:null!==n?n:c?c.phaseUpdatedAt:0,text:c&&c.text||"",lastSeq:c?this._seqOf(c.lastSeq):-1,textAvailable:!c||!1!==c.textAvailable});return void this._emitAssistantStream(e,s,l,{active:!0,text:l.textAvailable&&l.text||null})}if("DELTA"===t.type){const i=this._assistantSlot(e,s),n=i&&i.phase||"WRITING",r="number"==typeof t.seq?t.seq:null;let a=i?this._seqOf(i.lastSeq):-1,o=!i||!1!==i.textAvailable,c=i&&i.text||"";if(null===r)o&&(c+=t.delta||"");else{if(r<=a)return void this.logger.debug(`Duplicate assistant DELTA ignored: room=${e}, stream=${s}, seq=${r}, lastSeq=${a}`);r===a+1?(o&&(c+=t.delta||""),a=r):(this.logger.debug(`Assistant DELTA gap — text dropped: room=${e}, stream=${s}, seq=${r}, lastSeq=${a}`),o=!1,c="",a=r)}const l=this._setAssistantSlot(e,s,t,{phase:n,progress:i&&i.progress||null,text:c,lastSeq:a,textAvailable:o});return void this._emitAssistantStream(e,s,l,{active:!0,text:l.textAvailable?l.text:null,delta:l.textAvailable&&t.delta||null,seq:r})}if("DONE"===t.type){const i=this._assistantSlot(e,s);i&&i.timer&&clearTimeout(i.timer),this._deleteAssistantSlot(e,s),this._tombstoneAssistantStream(e,s),this._emitAssistantStream(e,s,i,{active:!1,personaId:t.personaId||i&&i.personaId||null,phase:null,progress:null,label:null,status:t.status||null,messageId:t.messageId||null})}}}async _loadAssistantSnapshot(e,t){const s=this._assistantSnapshotInFlight.get(e);if(s&&s.generation===t)return s.task;const i=(async()=>{let s=null;try{const t=await this.apiClient.get(`/api/v1/rooms/${e}/assistant/streams`),i=this._unwrapSuccessResponse(t);s=i&&Array.isArray(i.streams)?i.streams:[]}catch(t){this.logger.warn(`Assistant stream snapshot failed (live-only fallback): room=${e}`,t)}if(this._assistantGeneration(e)!==t)return this.logger.debug(`Assistant snapshot discarded (stale generation): room=${e}`),void this._deleteAssistantPendingIfGeneration(e,t);s&&this._applyAssistantSnapshot(e,s),this._drainAssistantPending(e,t)})();this._assistantSnapshotInFlight.set(e,{generation:t,task:i});try{await i}finally{const s=this._assistantSnapshotInFlight.get(e);s&&s.generation===t&&this._assistantSnapshotInFlight.delete(e)}}_deleteAssistantPendingIfGeneration(e,t){const s=this._assistantSnapshotPending.get(e);s&&s.generation===t&&this._assistantSnapshotPending.delete(e)}_applyAssistantSnapshot(e,t){t.forEach(t=>{const s=t&&t.streamId;if(!s)return;if(this._isAssistantStreamClosed(e,s))return;if("DONE"===t.status)return this._tombstoneAssistantStream(e,s),void this._emitAssistantStream(e,s,null,{active:!1,phase:null,progress:null,label:null,personaId:t.personaId||null,status:t.doneStatus||null,messageId:t.messageId||null,resumed:!0});const i=!1!==t.textAvailable,n=this._setAssistantSlot(e,s,{personaId:t.personaId,expiresAt:void 0},{phase:t.phase||null,progress:this._normalizeAssistantProgress(t.progress),phaseUpdatedAt:"number"==typeof t.phaseUpdatedAt?t.phaseUpdatedAt:0,text:i&&t.text||"",lastSeq:this._seqOf(t.lastSeq),textAvailable:i,resumed:!0});this._emitAssistantStream(e,s,n,{active:!0,text:n.textAvailable&&n.text||null,resumed:!0})})}_drainAssistantPending(e,t){const s=this._assistantSnapshotPending.get(e);s&&(void 0!==t&&s.generation!==t||(this._assistantSnapshotPending.delete(e),s.events.forEach(t=>this._handleAssistantStreamEvent(e,t))))}_assistantGeneration(e){return this._assistantRoomGeneration.get(e)||0}_armAssistantSnapshot(e){const t=this._assistantGeneration(e)+1;this._assistantRoomGeneration.set(e,t);const s=this._assistantSnapshotPending.get(e);return this._assistantSnapshotPending.set(e,{generation:t,events:s?s.events:[]}),t}async resumeAssistantStreams(e="manual"){if(!this.assistantStreamReplay)return;const t=Array.from(this.subscribedRooms.keys());0!==t.length&&(this.logger.debug(`Assistant stream replay: rooms=${t.length}, reason=${e}`),await Promise.all(t.map(e=>{const t=this._armAssistantSnapshot(e);return this._loadAssistantSnapshot(e,t).catch(t=>this.logger.warn(`Assistant stream replay failed: room=${e}`,t))})))}_assistantSlots(e){let t=this._assistantProgress.get(e);return t||(t=new Map,this._assistantProgress.set(e,t)),t}_assistantSlot(e,t){const s=this._assistantProgress.get(e);return s?s.get(t):void 0}_deleteAssistantSlot(e,t){const s=this._assistantProgress.get(e);s&&(s.delete(t),0===s.size&&this._assistantProgress.delete(e))}_isAssistantStreamClosed(e,t){const s=this._assistantStreamTombstones.get(e);if(!s)return!1;const i=s.get(t);return void 0!==i&&(!(i<=Date.now())||(s.delete(t),!1))}_tombstoneAssistantStream(e,t){let s=this._assistantStreamTombstones.get(e);s||(s=new Map,this._assistantStreamTombstones.set(e,s));const i=Date.now();s.forEach((e,t)=>{e<=i&&s.delete(t)}),s.set(t,i+this._assistantTombstoneTtlMs),s.size>this._assistantTombstoneWarnThreshold?this._assistantTombstoneWarned.has(e)||(this._assistantTombstoneWarned.add(e),this.logger.warn(`Assistant tombstones above threshold — NOT pruning (TTL only): room=${e}, size=${s.size}, threshold=${this._assistantTombstoneWarnThreshold}`)):this._assistantTombstoneWarned.delete(e)}_handleSummaryEvent(e,t){t&&t.status&&this.emit("summaryUpdate",{roomId:e,status:t.status,format:t.format||null,languages:t.languages||null,updatedAt:t.updatedAt||null})}_seqOf(e){return"number"==typeof e?e:-1}_setAssistantSlot(e,t,s,i){const n=this._assistantSlot(e,t);n&&n.timer&&clearTimeout(n.timer);let r=this._assistantProgressFallbackTtlMs;"number"==typeof s.expiresAt&&(r=Math.min(12e4,Math.max(5e3,s.expiresAt-Date.now())));const a=setTimeout(()=>{this._expireAssistantStream(e,t)},r),o={personaId:s.personaId||n&&n.personaId||null,phase:i.phase,progress:i.progress||null,phaseUpdatedAt:void 0!==i.phaseUpdatedAt?i.phaseUpdatedAt:n&&n.phaseUpdatedAt||0,text:i.text||"",lastSeq:this._seqOf(i.lastSeq),textAvailable:!1!==i.textAvailable,resumed:!0===i.resumed||!!n&&!0===n.resumed,updatedAt:Date.now(),order:++this._assistantSlotSeq,timer:a};return this._assistantSlots(e).set(t,o),o}_expireAssistantStream(e,t){const s=this._assistantSlot(e,t);s&&(s.timer&&clearTimeout(s.timer),this._deleteAssistantSlot(e,t),this.logger.debug(`Assistant progress auto-cleared (timeout): room=${e}, stream=${t}`),this._emitAssistantStream(e,t,s,{active:!1,phase:null,progress:null,label:null,reason:"timeout"}))}_emitAssistantStream(e,t,s,i){const n="phase"in i?i.phase:s?s.phase:null,r="progress"in i?i.progress:s?s.progress:null;this.emit("assistantProgress",{roomId:e,streamId:t,personaId:s?s.personaId:null,phase:n,progress:r,label:this._assistantPhaseLabel(n,r),textAvailable:!s||s.textAvailable,lastSeq:s?s.lastSeq:-1,...i}),this._emitAssistantRoomProgress(e)}_emitAssistantRoomProgress(e){const{streams:t,...s}=this._assistantRoomProgressPayload(e);this.emit("assistantRoomProgress",s)}_assistantRoomProgressPayload(e){const t=this._assistantProgress.get(e),s=t?Array.from(t.entries()):[];if(0===s.length)return{roomId:e,active:!1,streamCount:0,streamId:null,personaId:null,phase:null,label:null,text:null,textAvailable:!0,progress:null,streams:[]};const[i,n]=s.reduce((e,t)=>t[1].order>e[1].order?t:e);return{roomId:e,active:!0,streamCount:s.length,streamId:i,personaId:n.personaId,phase:n.phase,progress:n.progress||null,label:this._assistantPhaseLabel(n.phase,n.progress),text:n.textAvailable&&n.text||null,textAvailable:n.textAvailable,streams:s.sort((e,t)=>e[1].order-t[1].order).map(([e,t])=>({streamId:e,personaId:t.personaId,phase:t.phase,progress:t.progress||null,label:this._assistantPhaseLabel(t.phase,t.progress),text:t.textAvailable&&t.text||null,textAvailable:t.textAvailable,resumed:!0===t.resumed}))}}_normalizeAssistantProgress(e){if(!e||!["ANALYZING","SUMMARIZING"].includes(e.stage))return null;if("SUMMARIZING"===e.stage)return{stage:"SUMMARIZING",current:null,total:null,unit:null};const t=Number.isInteger(e.current)?e.current:null,s=Number.isInteger(e.total)?e.total:null,i=["DOCUMENT","PAGE","TEXT_CHUNK"].includes(e.unit)?e.unit:null;return null===t||null===s||!i||t<0||s<1||t>s?null:{stage:"ANALYZING",current:t,total:s,unit:i}}_assistantPhaseLabel(e,t){if(!e)return null;if("DOCUMENT_ANALYSIS"!==e)return this._assistantPhaseLabels[e]||null;if(t&&"SUMMARIZING"===t.stage)return"분석 결과 종합 중…";if(!t||"ANALYZING"!==t.stage)return this._assistantPhaseLabels[e];const s="PAGE"===t.unit?"페이지":"TEXT_CHUNK"===t.unit?"구간":"개";return`문서 분석 중… (${t.current}/${t.total}${s})`}getAssistantProgress(e){return this._assistantRoomProgressPayload(e)}_clearAssistantProgress(e){const t=this._assistantProgress.get(e);t&&(t.forEach(e=>e.timer&&clearTimeout(e.timer)),this._assistantProgress.delete(e)),this._assistantStreamTombstones.delete(e),this._assistantTombstoneWarned.delete(e),this._assistantSnapshotPending.delete(e),this._assistantRoomGeneration.set(e,(this._assistantRoomGeneration.get(e)||0)+1)}getSubscribedRooms(){return Array.from(this.subscribedRooms.keys())}isSubscribed(e){return this.subscribedRooms.has(e)}setLogLevel(e){this.logger.setLevel(e)}destroy(){this.unsubscribeAllRooms(),this.roomListSubscribed&&this.unsubscribeRoomList(),this._typingTimers.forEach(e=>clearTimeout(e)),this._typingTimers.clear(),this._assistantTypingTimers.forEach(e=>clearTimeout(e)),this._assistantTypingTimers.clear(),this._assistantProgress.forEach(e=>e.forEach(e=>e.timer&&clearTimeout(e.timer))),this._assistantProgress.clear(),this._assistantStreamTombstones.clear(),this._assistantTombstoneWarned.clear(),this._assistantRoomGeneration.clear(),this._assistantSnapshotPending.clear(),this._assistantSnapshotInFlight.clear(),this._seenChatMessageIdsByRoom.clear(),this._seenRoomListMessageIdsByRoom.clear(),this.connectionManager&&"function"==typeof this.connectionManager.off&&(this._onReconnectedForSync&&this.connectionManager.off("reconnected",this._onReconnectedForSync),this._onResumedForSync&&this.connectionManager.off("resumed",this._onResumedForSync)),this._anchor.clear(),this._anchorGen.clear(),this._syncInFlight.clear(),this.removeAllListeners(),this.logger.info("ChatClient destroyed")}}class D extends i{constructor(e={}){super(),this.localStream=null,this.screenStream=null,this.videoEnabled=!0,this.audioEnabled=!0,this.logger=new m(e.logLevel||p.WARN,"MediaStreamManager"),this._deviceChangeHandler=null}async getUserMedia(e={video:!0,audio:!0}){try{return this.localStream=await navigator.mediaDevices.getUserMedia(e),this.videoEnabled=!1!==e.video,this.audioEnabled=!1!==e.audio,this.emit("streamStarted",{stream:this.localStream}),this.logger.info("User media stream started"),this.localStream}catch(e){throw this.logger.error("Failed to get user media:",e),this.emit("error",{type:r.MEDIA_ACCESS_DENIED,message:this._getMediaErrorMessage(e),error:e}),e}}_getMediaErrorMessage(e){switch(e.name){case"NotAllowedError":return"Camera/Microphone permission denied";case"NotFoundError":return"Camera/Microphone not found";case"NotReadableError":return"Camera/Microphone is already in use";case"OverconstrainedError":return"Camera/Microphone constraints cannot be satisfied";case"AbortError":return"Media access aborted";default:return e.message||"Unknown media error"}}async getDisplayMedia(e={video:!0,audio:!1}){try{return this.screenStream=await navigator.mediaDevices.getDisplayMedia(e),this.screenStream.getVideoTracks()[0].onended=()=>{this.emit("screenShareEnded",{}),this.screenStream=null},this.emit("screenShareStarted",{stream:this.screenStream}),this.logger.info("Screen share stream started"),this.screenStream}catch(e){throw this.logger.error("Failed to get display media:",e),this.emit("error",{type:r.SCREEN_SHARE_DENIED,message:"NotAllowedError"===e.name?"Screen share permission denied":e.message,error:e}),e}}toggleVideo(){if(!this.localStream)return this.logger.warn("No local stream available"),this.videoEnabled;const e=this.localStream.getVideoTracks();return 0===e.length?(this.logger.warn("No video track available"),this.videoEnabled):(this.videoEnabled=!this.videoEnabled,e.forEach(e=>{e.enabled=this.videoEnabled}),this.emit("videoToggled",{enabled:this.videoEnabled}),this.logger.debug(`Video toggled: ${this.videoEnabled}`),this.videoEnabled)}toggleAudio(){if(!this.localStream)return this.logger.warn("No local stream available"),this.audioEnabled;const e=this.localStream.getAudioTracks();return 0===e.length?(this.logger.warn("No audio track available"),this.audioEnabled):(this.audioEnabled=!this.audioEnabled,e.forEach(e=>{e.enabled=this.audioEnabled}),this.emit("audioToggled",{enabled:this.audioEnabled}),this.logger.debug(`Audio toggled: ${this.audioEnabled}`),this.audioEnabled)}setVideoEnabled(e){if(!this.localStream)return void this.logger.warn("No local stream available");this.localStream.getVideoTracks().forEach(t=>{t.enabled=e}),this.videoEnabled=e,this.emit("videoToggled",{enabled:e})}setAudioEnabled(e){if(!this.localStream)return void this.logger.warn("No local stream available");this.localStream.getAudioTracks().forEach(t=>{t.enabled=e}),this.audioEnabled=e,this.emit("audioToggled",{enabled:e})}getLocalStream(){return this.localStream}getScreenStream(){return this.screenStream}isVideoEnabled(){return this.videoEnabled}isAudioEnabled(){return this.audioEnabled}stopAll(){this.localStream&&(this.localStream.getTracks().forEach(e=>e.stop()),this.localStream=null,this.emit("streamStopped",{}),this.logger.info("All media tracks stopped")),this.screenStream&&(this.screenStream.getTracks().forEach(e=>e.stop()),this.screenStream=null)}replaceTrack(e,t){this.localStream?(this.localStream.removeTrack(e),this.localStream.addTrack(t),e.stop(),this.emit("trackReplaced",{oldTrack:e,newTrack:t}),this.logger.debug(`Track replaced: ${e.kind}`)):this.logger.warn("No local stream available")}getTrack(e){if(!this.localStream)return null;const t="video"===e?this.localStream.getVideoTracks():this.localStream.getAudioTracks();return t.length>0?t[0]:null}async getDevices(){try{const e=await navigator.mediaDevices.enumerateDevices();return{videoInputs:e.filter(e=>"videoinput"===e.kind),audioInputs:e.filter(e=>"audioinput"===e.kind),audioOutputs:e.filter(e=>"audiooutput"===e.kind)}}catch(e){throw this.logger.error("Failed to enumerate devices:",e),this.emit("error",{type:r.ENUMERATE_DEVICES_FAILED,message:e.message,error:e}),e}}async switchDevice(e,t){if(!this.localStream)throw new Error("No local stream available");try{const s="video"===t?{video:{deviceId:{exact:e}},audio:!1}:{video:!1,audio:{deviceId:{exact:e}}},i=(await navigator.mediaDevices.getUserMedia(s)).getTracks()[0],n="video"===t?this.localStream.getVideoTracks()[0]:this.localStream.getAudioTracks()[0];return n&&(this.replaceTrack(n,i),this.emit("deviceSwitched",{kind:t,deviceId:e,newTrack:i}),this.logger.info(`${t} device switched to ${e}`)),i}catch(e){throw this.logger.error(`Failed to switch ${t} device:`,e),this.emit("error",{type:r.DEVICE_SWITCH_FAILED,message:e.message,error:e}),e}}startDeviceChangeDetection(){this._deviceChangeHandler||(this._deviceChangeHandler=async()=>{try{const e=await this.getDevices();this.emit("deviceChange",e),this.logger.debug("Device change detected")}catch(e){this.logger.warn("Failed to get devices on change:",e)}},navigator.mediaDevices.addEventListener("devicechange",this._deviceChangeHandler),this.logger.debug("Device change detection started"))}stopDeviceChangeDetection(){this._deviceChangeHandler&&(navigator.mediaDevices.removeEventListener("devicechange",this._deviceChangeHandler),this._deviceChangeHandler=null,this.logger.debug("Device change detection stopped"))}getStreamInfo(){return this.localStream?{hasStream:!0,videoEnabled:this.videoEnabled,audioEnabled:this.audioEnabled,tracks:this.localStream.getTracks().map(e=>({kind:e.kind,id:e.id,label:e.label,enabled:e.enabled,readyState:e.readyState,muted:e.muted}))}:{hasStream:!1,videoEnabled:this.videoEnabled,audioEnabled:this.audioEnabled,tracks:[]}}async applyVideoConstraints(e){if(!this.localStream)throw new Error("No local stream available");const t=this.localStream.getVideoTracks()[0];if(!t)throw new Error("No video track available");try{await t.applyConstraints(e),this.emit("videoConstraintsApplied",{constraints:e}),this.logger.info("Video constraints applied:",e)}catch(e){throw this.logger.error("Failed to apply video constraints:",e),e}}getVideoSettings(){if(!this.localStream)return null;const e=this.localStream.getVideoTracks()[0];return e?e.getSettings():null}getAudioSettings(){if(!this.localStream)return null;const e=this.localStream.getAudioTracks()[0];return e?e.getSettings():null}setLogLevel(e){this.logger.setLevel(e)}destroy(){this.stopAll(),this.stopDeviceChangeDetection(),this.removeAllListeners(),this.logger.info("MediaStreamManager destroyed")}}class P extends i{constructor(e={}){super(),this.iceServers=e.iceServers||g.iceServers,this.logger=new m(e.logLevel||p.WARN,"PeerConnectionManager"),this.peers=new Map,this.localStream=null}setLocalStream(e){this.localStream=e,this.peers.forEach(e=>{this._addTracksToConnection(e.connection)})}setIceServers(e){this.iceServers=e}createPeerConnection(e,t=!1,s={}){if(this.peers.has(e))return this.logger.warn(`Peer connection already exists: ${e}`),this.peers.get(e).connection;const i={iceServers:this.iceServers,iceCandidatePoolSize:10},n=new RTCPeerConnection(i),r={connection:n,polite:t,makingOffer:!1,ignoreOffer:!1,isSettingRemoteAnswerPending:!1,skipAutoNegotiation:s.skipAutoNegotiation||!1};return this.peers.set(e,r),this._setupConnectionHandlers(e,n,r),this.localStream&&this._addTracksToConnection(n),this.logger.info(`Peer connection created: ${e} (polite: ${t}, skipAutoNegotiation: ${r.skipAutoNegotiation})`),n}_setupConnectionHandlers(e,t,s){t.onicecandidate=t=>{t.candidate&&this.emit("iceCandidate",{peerId:e,candidate:t.candidate})},t.oniceconnectionstatechange=()=>{this.logger.debug(`ICE connection state (${e}): ${t.iceConnectionState}`),this.emit("iceConnectionStateChange",{peerId:e,state:t.iceConnectionState}),"failed"===t.iceConnectionState&&this.emit("error",{type:r.ICE_CONNECTION_FAILED,peerId:e,message:"ICE connection failed"})},t.onconnectionstatechange=()=>{this.logger.debug(`Connection state (${e}): ${t.connectionState}`),this.emit("connectionStateChange",{peerId:e,state:t.connectionState}),"connected"===t.connectionState?this.emit("peerConnected",{peerId:e}):"disconnected"!==t.connectionState&&"failed"!==t.connectionState||this.emit("peerDisconnected",{peerId:e})},t.onnegotiationneeded=async()=>{if(s.skipAutoNegotiation)this.logger.debug(`Skipping auto negotiation for ${e} (manual offer will be sent)`);else try{s.makingOffer=!0,await t.setLocalDescription(),this.emit("negotiationNeeded",{peerId:e,description:t.localDescription})}catch(t){this.logger.error(`Negotiation error (${e}):`,t)}finally{s.makingOffer=!1}},t.ontrack=t=>{this.logger.info(`Remote track received from ${e}:`,t.track.kind),this.emit("remoteTrack",{peerId:e,track:t.track,streams:t.streams})},t.ondatachannel=t=>{this.logger.info(`Data channel received from ${e}`),this.emit("dataChannel",{peerId:e,channel:t.channel})}}_addTracksToConnection(e){if(!this.localStream)return;const t=e.getSenders();this.localStream.getTracks().forEach(s=>{t.find(e=>e.track&&e.track.kind===s.kind)||e.addTrack(s,this.localStream)})}async createOffer(e){const t=this.peers.get(e);if(!t)throw new Error(`Peer not found: ${e}`);try{const s=await t.connection.createOffer();return await t.connection.setLocalDescription(s),this.logger.debug(`Offer created for ${e}`),t.connection.localDescription}catch(t){throw this.logger.error(`Failed to create offer for ${e}:`,t),t}}async createAnswer(e){const t=this.peers.get(e);if(!t)throw new Error(`Peer not found: ${e}`);try{const s=await t.connection.createAnswer();return await t.connection.setLocalDescription(s),this.logger.debug(`Answer created for ${e}`),t.connection.localDescription}catch(t){throw this.logger.error(`Failed to create answer for ${e}:`,t),t}}async handleRemoteDescription(e,t){const s=this.peers.get(e);if(!s)throw new Error(`Peer not found: ${e}`);const{connection:i,polite:n,makingOffer:r}=s,a="offer"===t.type&&(r||"stable"!==i.signalingState);if(s.ignoreOffer=!n&&a,s.ignoreOffer)this.logger.debug(`Ignoring offer collision from ${e} (I am impolite)`);else try{if(a&&n&&(this.logger.debug(`Offer collision detected, rolling back local offer for ${e} (I am polite)`),await i.setLocalDescription({type:"rollback"})),s.isSettingRemoteAnswerPending="answer"===t.type,await i.setRemoteDescription(t),s.isSettingRemoteAnswerPending=!1,this.logger.debug(`Remote ${t.type} set for ${e}`),"offer"===t.type){const t=await this.createAnswer(e);this.emit("answerCreated",{peerId:e,answer:t})}}catch(t){throw this.logger.error(`Failed to set remote description for ${e}:`,t),t}}async addIceCandidate(e,t){const s=this.peers.get(e);if(!s)return this.logger.warn(`Peer not found for ICE candidate: ${e}`),!1;if(!s.connection.remoteDescription)return this.logger.debug(`Remote description not set yet for ${e}, ICE candidate queued`),!1;try{return await s.connection.addIceCandidate(t),this.logger.debug(`ICE candidate added for ${e}`),!0}catch(t){return s.ignoreOffer||this.logger.error(`Failed to add ICE candidate for ${e}:`,t),!1}}getPeerConnection(e){const t=this.peers.get(e);return t?t.connection:null}getPeerIds(){return Array.from(this.peers.keys())}closePeerConnection(e){const t=this.peers.get(e);t&&(t.connection.close(),this.peers.delete(e),this.emit("peerClosed",{peerId:e}),this.logger.info(`Peer connection closed: ${e}`))}closeAllPeerConnections(){this.peers.forEach((e,t)=>{e.connection.close(),this.logger.debug(`Peer connection closed: ${t}`)}),this.peers.clear(),this.emit("allPeersClosed",{})}async replaceTrack(e,t){const s=[];this.peers.forEach((i,n)=>{const r=i.connection.getSenders().find(t=>t.track&&t.track.kind===e.kind);r&&s.push(r.replaceTrack(t).then(()=>this.logger.debug(`Track replaced for ${n}`)).catch(e=>this.logger.error(`Failed to replace track for ${n}:`,e)))}),await Promise.all(s)}async getStats(e){const t=this.peers.get(e);return t?t.connection.getStats():null}getConnectionSummary(){const e={};return this.peers.forEach((t,s)=>{e[s]={connectionState:t.connection.connectionState,iceConnectionState:t.connection.iceConnectionState,signalingState:t.connection.signalingState,polite:t.polite}}),e}setLogLevel(e){this.logger.setLevel(e)}destroy(){this.closeAllPeerConnections(),this.localStream=null,this.removeAllListeners(),this.logger.info("PeerConnectionManager destroyed")}}class N extends i{constructor(e){super(),this.connectionManager=e.connectionManager,this.apiClient=e.apiClient,this.userId=e.userId,this.logLevel=e.logLevel||p.WARN,this.logger=new m(this.logLevel,"WebRTCClient"),this.mediaManager=new D({logLevel:this.logLevel}),this.peerManager=new P({iceServers:e.iceServers||g.iceServers,logLevel:this.logLevel}),this.currentRoom=null,this.isGroupCall=!1,this.participants=new Map,this._iceServersFetched=!1,this._waitingForCallAccept=!1,this._pendingCallTarget=null,this._pendingIceCandidates=new Map,this._setupEventHandlers()}async initializeIceServers(){if(!this._iceServersFetched)try{const e=await this.getTurnCredentials();e&&e.iceServers&&(this.peerManager.setIceServers(e.iceServers),this._iceServersFetched=!0,this.logger.info("ICE servers fetched successfully"))}catch(e){this.logger.warn("Failed to fetch TURN credentials, using fallback ICE servers:",e.message),this._setFallbackIceServers()}}_setFallbackIceServers(){this.peerManager.setIceServers([{urls:"stun:stun.l.google.com:19302"},{urls:"stun:stun1.l.google.com:19302"}])}_setupEventHandlers(){this.mediaManager.on("streamStarted",e=>this.emit("localStreamStarted",e)),this.mediaManager.on("streamStopped",e=>this.emit("localStreamStopped",e)),this.mediaManager.on("videoToggled",()=>this._sendMediaState()),this.mediaManager.on("audioToggled",()=>this._sendMediaState()),this.mediaManager.on("screenShareStarted",e=>this.emit("screenShareStarted",e)),this.mediaManager.on("screenShareEnded",e=>this.emit("screenShareEnded",e)),this.mediaManager.on("error",e=>this.emit("error",e)),this.peerManager.on("remoteTrack",e=>this.emit("remoteTrack",e)),this.peerManager.on("peerConnected",e=>this.emit("peerConnected",e)),this.peerManager.on("peerDisconnected",e=>this.emit("peerDisconnected",e)),this.peerManager.on("peerClosed",e=>this.emit("peerClosed",e)),this.peerManager.on("error",e=>this.emit("error",e)),this.peerManager.on("iceCandidate",({peerId:e,candidate:t})=>{this._sendSignal({type:a.ICE_CANDIDATE,receiverId:e,data:{candidate:t}})}),this.peerManager.on("negotiationNeeded",({peerId:e,description:t})=>{this._sendSignal({type:a.CALL_OFFER,receiverId:e,data:{sdp:t}})}),this.peerManager.on("answerCreated",({peerId:e,answer:t})=>{this._sendSignal({type:a.CALL_ANSWER,receiverId:e,data:{sdp:t}})})}async getTurnCredentials(){return(await this.apiClient.get("/api/v1/webrtc/turn-credentials")).data}async createCallRoom(e){return this.apiClient.post("/api/v1/webrtc/rooms",e)}async getCallRoom(e){return this.apiClient.get(`/api/v1/webrtc/rooms/${e}`)}async joinCallRoomApi(e){return this.apiClient.post(`/api/v1/webrtc/rooms/${e}/join`)}async leaveCallRoomApi(e){return this.apiClient.delete(`/api/v1/webrtc/rooms/${e}/leave`)}async enableIncomingCalls(){this._incomingCallsEnabled?this.logger.debug("Incoming calls already enabled"):(await this._subscribeToDirectSignaling(),this._incomingCallsEnabled=!0,this.logger.info("Incoming calls enabled - now listening for call invitations"))}disableIncomingCalls(){if(!this._incomingCallsEnabled)return;const e=l.getWebRTCUserDestination();this.connectionManager.unsubscribe(e),this._incomingCallsEnabled=!1,this.logger.info("Incoming calls disabled")}isIncomingCallsEnabled(){return this._incomingCallsEnabled||!1}async startCall(e){const{roomId:t,isGroup:s=!1,mediaConstraints:i={video:!0,audio:!0}}=e;try{await this.initializeIceServers();const e=await this.mediaManager.getUserMedia(i);return this.peerManager.setLocalStream(e),this.currentRoom=t,this.isGroupCall=s,await this._subscribeToSignaling(t,s),this._sendSignal({type:a.JOIN_ROOM,roomId:t}),this.logger.info(`Call started in room: ${t}`),this.emit("callStarted",{roomId:t,isGroup:s,localStream:e}),e}catch(e){throw this.logger.error("Failed to start call:",e),this.emit("error",{type:r.PEER_CONNECTION_FAILED,message:e.message,error:e}),e}}async callUser(e,t={video:!0,audio:!0}){try{await this.initializeIceServers();const s=await this.mediaManager.getUserMedia(t);this.peerManager.setLocalStream(s),await this._subscribeToDirectSignaling(),this._waitingForCallAccept=!0,this._pendingCallTarget=e,this._sendSignal({type:a.CALL_REQUEST,receiverId:e}),this.currentRoom=null,this.isGroupCall=!1,this.emit("callRequested",{targetUserId:e,localStream:s})}catch(e){throw this.logger.error("Failed to call user:",e),this._waitingForCallAccept=!1,this._pendingCallTarget=null,e}}async acceptCall(e,t={video:!0,audio:!0}){try{await this.initializeIceServers();const s=await this.mediaManager.getUserMedia(t);this.peerManager.setLocalStream(s),this.peerManager.createPeerConnection(e,!0,{skipAutoNegotiation:!0}),this._sendSignal({type:a.CALL_ACCEPT,receiverId:e}),this.emit("callAccepted",{callerId:e})}catch(e){throw this.logger.error("Failed to accept call:",e),e}}rejectCall(e){this._sendSignal({type:a.CALL_REJECT,receiverId:e}),this.emit("callRejected",{callerId:e})}cancelCall(){this._waitingForCallAccept&&this._pendingCallTarget&&(this._sendSignal({type:a.CALL_CANCEL,receiverId:this._pendingCallTarget}),this.emit("callCancelled",{targetUserId:this._pendingCallTarget}),this._waitingForCallAccept=!1,this._pendingCallTarget=null,this.mediaManager.stopAll())}endCall(){this._waitingForCallAccept&&this._pendingCallTarget&&this._sendSignal({type:a.CALL_CANCEL,receiverId:this._pendingCallTarget}),this._waitingForCallAccept=!1,this._pendingCallTarget=null,this.currentRoom&&(this._sendSignal({type:a.LEAVE_ROOM,roomId:this.currentRoom}),this._unsubscribeFromSignaling()),this.peerManager.getPeerIds().forEach(e=>{this._sendSignal({type:a.CALL_END,receiverId:e})}),this.peerManager.closeAllPeerConnections(),this.mediaManager.stopAll(),this.participants.clear(),this._pendingIceCandidates.clear();const e=this.currentRoom;this.currentRoom=null,this.isGroupCall=!1,this.emit("callEnded",{roomId:e}),this.logger.info("Call ended")}toggleVideo(){return this.mediaManager.toggleVideo()}toggleAudio(){return this.mediaManager.toggleAudio()}async startScreenShare(){const e=await this.mediaManager.getDisplayMedia(),t=e.getVideoTracks()[0],s=this.mediaManager.getTrack("video");return s&&t&&await this.peerManager.replaceTrack(s,t),t.onended=async()=>{const e=this.mediaManager.getTrack("video");e&&await this.peerManager.replaceTrack(t,e),this.emit("screenShareEnded",{})},e}stopScreenShare(){const e=this.mediaManager.getScreenStream();e&&e.getTracks().forEach(e=>e.stop())}getLocalStream(){return this.mediaManager.getLocalStream()}async getDevices(){return this.mediaManager.getDevices()}async switchDevice(e,t){const s=this.mediaManager.getTrack(t),i=await this.mediaManager.switchDevice(e,t);return s&&await this.peerManager.replaceTrack(s,i),i}setVideoEnabled(e){this.mediaManager.setVideoEnabled(e),this._sendMediaState()}setAudioEnabled(e){this.mediaManager.setAudioEnabled(e),this._sendMediaState()}startDeviceChangeDetection(){this.mediaManager.startDeviceChangeDetection(),this.mediaManager.on("deviceChange",e=>{this.emit("deviceChange",e)})}stopDeviceChangeDetection(){this.mediaManager.stopDeviceChangeDetection()}async applyVideoConstraints(e){return this.mediaManager.applyVideoConstraints(e)}getVideoSettings(){return this.mediaManager.getVideoSettings()}getAudioSettings(){return this.mediaManager.getAudioSettings()}async _subscribeToSignaling(e,t){if(t){const t=l.getWebRTCDestination(e);await this.connectionManager.subscribe(t,e=>{this._handleSignal(e)})}await this._subscribeToDirectSignaling()}async _subscribeToDirectSignaling(){const e=l.getWebRTCUserDestination();await this.connectionManager.subscribe(e,e=>{this._handleSignal(e)})}_unsubscribeFromSignaling(){if(this.currentRoom&&this.isGroupCall){const e=l.getWebRTCDestination(this.currentRoom);this.connectionManager.unsubscribe(e)}if(!this._incomingCallsEnabled){const e=l.getWebRTCUserDestination();this.connectionManager.unsubscribe(e)}}_sendSignal(e){const t={type:e.type,roomId:e.roomId||this.currentRoom,receiverId:e.receiverId,data:e.data};this.connectionManager.send(l.WEBRTC_SIGNAL,t),this.logger.debug("Signal sent:",t)}_sendMediaState(){if(!this.currentRoom&&0===this.peerManager.getPeerIds().length)return;const e={videoEnabled:this.mediaManager.isVideoEnabled(),audioEnabled:this.mediaManager.isAudioEnabled()};this._sendSignal({type:a.VIDEO_STATE_CHANGED,data:e}),this.emit("mediaStateChanged",e)}async _handleSignal(e){const{type:t,senderId:s,data:i}=e;if(s!==this.userId)switch(this.logger.debug(`Signal received: ${t} from ${s}`),t){case a.JOIN_ROOM:case a.PEER_JOINED:await this._handleUserJoined(s);break;case a.LEAVE_ROOM:case a.PEER_LEFT:this._handleUserLeft(s);break;case a.CALL_OFFER:await this._handleOffer(s,i.sdp);break;case a.CALL_ANSWER:await this._handleAnswer(s,i.sdp);break;case a.ICE_CANDIDATE:await this._handleIceCandidate(s,i.candidate);break;case a.VIDEO_STATE_CHANGED:case a.AUDIO_STATE_CHANGED:this._handleMediaState(s,i);break;case a.CALL_REQUEST:this.isInCall()||this._waitingForCallAccept?(this._sendSignal({type:a.CALL_BUSY,receiverId:s}),this.emit("incomingCallWhileBusy",{callerId:s})):this.emit("incomingCall",{callerId:s});break;case a.CALL_BUSY:this._waitingForCallAccept=!1,this._pendingCallTarget=null,this.mediaManager.stopAll(),this.emit("callBusy",{userId:s});break;case a.CALL_INVITATION:this.emit("callInvitation",{callRoomId:i?.callRoomId||e.roomId,title:i?.title,hostUserId:i?.hostUserId||s,maxParticipants:i?.maxParticipants,createdAt:i?.createdAt});break;case a.CALL_ACCEPT:await this._handleCallAccepted(s);break;case a.CALL_REJECT:this._waitingForCallAccept=!1,this._pendingCallTarget=null,this.mediaManager.stopAll(),this.emit("callRejected",{userId:s});break;case a.CALL_CANCEL:this._waitingForCallAccept=!1,this._pendingCallTarget=null,this.emit("callCancelled",{userId:s});break;case a.CALL_END:this._handleCallEnded(s);break;default:this.logger.warn(`Unknown signal type: ${t}`)}}async _handleUserJoined(e){this.participants.set(e,{joinedAt:new Date});const t=this.userId<e;this.peerManager.createPeerConnection(e,t),this.emit("userJoined",{userId:e}),this.logger.info(`User joined: ${e} (I am ${t?"polite":"impolite"})`)}_handleUserLeft(e){this.participants.delete(e),this.peerManager.closePeerConnection(e),this.emit("userLeft",{userId:e}),this.logger.info(`User left: ${e}`)}async _handleOffer(e,t){if(!this.peerManager.getPeerConnection(e)){const t=this.userId<e;this.peerManager.createPeerConnection(e,t,{skipAutoNegotiation:!0})}await this.peerManager.handleRemoteDescription(e,t),await this._processPendingIceCandidates(e)}async _handleAnswer(e,t){await this.peerManager.handleRemoteDescription(e,t),await this._processPendingIceCandidates(e)}async _handleIceCandidate(e,t){if(!this.peerManager.getPeerConnection(e))return void this._queueIceCandidate(e,t);await this.peerManager.addIceCandidate(e,new RTCIceCandidate(t))||this._queueIceCandidate(e,t)}_queueIceCandidate(e,t){this._pendingIceCandidates.has(e)||this._pendingIceCandidates.set(e,[]),this._pendingIceCandidates.get(e).push(t),this.logger.debug(`ICE candidate queued for ${e}`)}async _processPendingIceCandidates(e){const t=this._pendingIceCandidates.get(e);if(t&&0!==t.length){this.logger.debug(`Processing ${t.length} pending ICE candidates for ${e}`);for(const s of t)await this.peerManager.addIceCandidate(e,new RTCIceCandidate(s));this._pendingIceCandidates.delete(e)}}_handleMediaState(e,t){this.emit("participantMediaState",{userId:e,videoEnabled:t.videoEnabled,audioEnabled:t.audioEnabled})}async _handleCallAccepted(e){this._waitingForCallAccept=!1,this._pendingCallTarget=null,this.peerManager.createPeerConnection(e,!1,{skipAutoNegotiation:!0});const t=await this.peerManager.createOffer(e);this._sendSignal({type:a.CALL_OFFER,receiverId:e,data:{sdp:t}}),this.emit("callAccepted",{userId:e})}_handleCallEnded(e){this.peerManager.closePeerConnection(e),this.participants.delete(e),this.emit("participantLeft",{userId:e}),0===this.peerManager.getPeerIds().length&&this.endCall()}isInCall(){return null!==this.currentRoom||this.peerManager.getPeerIds().length>0}getCurrentRoom(){return this.currentRoom}getParticipants(){return Array.from(this.participants.keys())}getConnectionSummary(){return this.peerManager.getConnectionSummary()}async getStats(e){return this.peerManager.getStats(e)}getMediaState(){return{videoEnabled:this.mediaManager.isVideoEnabled(),audioEnabled:this.mediaManager.isAudioEnabled(),streamInfo:this.mediaManager.getStreamInfo()}}setLogLevel(e){this.logger.setLevel(e),this.mediaManager.setLogLevel(e),this.peerManager.setLogLevel(e)}destroy(){this.endCall(),this.mediaManager.destroy(),this.peerManager.destroy(),this.removeAllListeners(),this.logger.info("WebRTCClient destroyed")}}const L=Object.freeze({UNSUPPORTED_BROWSER:"UNSUPPORTED_BROWSER",FIREBASE_NOT_INSTALLED:"FIREBASE_NOT_INSTALLED",SW_REGISTER_FAILED:"SW_REGISTER_FAILED",PERMISSION_DENIED:"PERMISSION_DENIED",TOKEN_FAILED:"TOKEN_FAILED",SERVER_REGISTER_FAILED:"SERVER_REGISTER_FAILED"});class O extends Error{constructor(e,t,s){super(t),this.name="PushError",this.code=e,void 0!==s&&(this.cause=s)}}const U={apiKey:"AIzaSyB8VhRg6rSvUI7K3Ua7h6sBpLvaQGmIkRc",authDomain:"chatting-c3e5d.firebaseapp.com",projectId:"chatting-c3e5d",storageBucket:"chatting-c3e5d.firebasestorage.app",messagingSenderId:"1020496565673",appId:"1:1020496565673:web:5039167257fd83f5ce20b8"},$="pendingNavigation",x="pending-room",F="deviceInfo",j="device-id",W="talkflow_device_id";function G(){return"undefined"==typeof indexedDB?Promise.resolve(null):new Promise((e,t)=>{const s=indexedDB.open("talkflowPush",2);s.onupgradeneeded=()=>{const e=s.result;e.objectStoreNames.contains($)||e.createObjectStore($,{keyPath:"id"}),e.objectStoreNames.contains(F)||e.createObjectStore(F,{keyPath:"id"})},s.onsuccess=()=>e(s.result),s.onerror=()=>t(s.error||new Error("IndexedDB open failed"))})}function V(){return G().then(e=>e?new Promise((t,s)=>{const i=e.transaction(F,"readonly"),n=i.objectStore(F).get(j);n.onsuccess=()=>{const e=n.result;t(e&&e.value?e.value:null)},n.onerror=()=>s(n.error||new Error("IndexedDB read failed")),i.oncomplete=()=>e.close(),i.onerror=()=>e.close(),i.onabort=()=>e.close()}):null)}function q(e){return G().then(t=>!!t&&new Promise(s=>{try{const i=t.transaction(F,"readwrite");i.objectStore(F).put({id:j,value:e}),i.oncomplete=()=>{t.close(),s(!0)},i.onerror=()=>{t.close(),s(!1)},i.onabort=()=>{t.close(),s(!1)}}catch(e){try{t.close()}catch(e){}s(!1)}})).catch(()=>!1)}function J(){return"web-"+("undefined"!=typeof crypto&&crypto.randomUUID?crypto.randomUUID():Date.now().toString(36)+Math.random().toString(36).substring(2))}async function H(e){const t=await G();if(!t)return null;const s=function(e){return e&&"string"==typeof e&&""!==e.trim()?"pending-room:"+e:x}(e),i=!!e&&s!==x;return new Promise((n,r)=>{const a=t.transaction($,"readwrite"),o=a.objectStore($);let c=null;const l=o.get(s);l.onsuccess=()=>{const t=l.result;if(t&&t.roomId){if((t.projectId||null)===(e||null))return c=t.roomId,void o.delete(s)}if(!i)return;const n=o.get(x);n.onsuccess=()=>{const e=n.result;e&&e.roomId&&!e.projectId&&(c=e.roomId,o.delete(x))}},l.onerror=()=>{r(l.error||new Error("IndexedDB read failed"))},a.oncomplete=()=>{t.close(),n(c)},a.onerror=()=>{t.close(),r(a.error||new Error("IndexedDB transaction failed"))},a.onabort=()=>{t.close(),r(a.error||new Error("IndexedDB transaction aborted"))}})}class K{constructor(e){this.apiClient=e.apiClient,this.projectId=e.projectId||null,this.firebaseConfig=e.firebaseConfig||U,this.vapidKey=e.vapidKey||"BGP8qaSm1ntjWd4n9pc0lX_rw4BmMxm9u4pvRIANCitbmYaV0iy-gn05suTKBo88kUBejxdxM8sb6x2nt3avu8c",this.serviceWorkerPath=e.serviceWorkerPath||"/firebase-messaging-sw.js",this.logger=new m(e.logLevel||p.WARN,"PushManager"),this._messaging=null,this._currentToken=null,this._enabled=!1,this._enablePromise=null,this._foregroundMessageUnsubscribe=null,this._swRegistration=null,this._projectRegisteredToSW=!1,this._visibilityHandler=null,this._deviceIdCache=null}static async consumePendingRoom(e){return H(e)}static getPermissionState(){return"undefined"==typeof window||"undefined"==typeof Notification?"unsupported":"undefined"!=typeof navigator&&"serviceWorker"in navigator?Notification.permission:"unsupported"}async enable(){if(this._enabled)this.logger.debug("푸시 알림이 이미 활성화되어 있습니다.");else{if(this._enablePromise)return this.logger.debug("푸시 알림 활성화가 이미 진행 중입니다."),this._enablePromise;this._enablePromise=this._enableInternal();try{return await this._enablePromise}finally{this._enablePromise=null}}}async _enableInternal(){if("undefined"==typeof window||!("Notification"in window))throw new O(L.UNSUPPORTED_BROWSER,"푸시 알림은 브라우저 환경에서만 사용 가능합니다");if(!("serviceWorker"in navigator))throw new O(L.UNSUPPORTED_BROWSER,"이 브라우저는 서비스 워커를 지원하지 않습니다");if("denied"===Notification.permission)throw new O(L.PERMISSION_DENIED,"알림 권한이 이미 거부되어 있습니다. 브라우저 사이트 설정에서 허용해주세요.");let e,t,s;try{e=await import("firebase/app"),t=await import("firebase/messaging")}catch(e){throw new O(L.FIREBASE_NOT_INSTALLED,"firebase 패키지가 설치되지 않았습니다. npm install firebase 를 실행하세요.",e)}try{s=e.getApp("talkflow")}catch{s=e.initializeApp(this.firebaseConfig,"talkflow")}const i=await this._registerServiceWorker();if("granted"!==("granted"===Notification.permission?"granted":await Notification.requestPermission()))throw new O(L.PERMISSION_DENIED,"알림 권한이 거부되었습니다. 브라우저 설정에서 허용해주세요.");this._messaging=t.getMessaging(s);const n={serviceWorkerRegistration:i};this.vapidKey&&(n.vapidKey=this.vapidKey);try{this._currentToken=await t.getToken(this._messaging,n)}catch(e){throw new O(L.TOKEN_FAILED,"FCM 토큰 획득 중 오류 발생: "+(e?.message||e),e)}if(!this._currentToken)throw new O(L.TOKEN_FAILED,"FCM 토큰 획득 실패 (응답이 비어있음)");await this._registerTokenToServer(this._currentToken),this._foregroundMessageUnsubscribe&&this._foregroundMessageUnsubscribe(),this._foregroundMessageUnsubscribe=t.onMessage(this._messaging,e=>{this.logger.debug("포그라운드 푸시 수신:",e),this._onForegroundMessage(e)}),this._swRegistration=i,await this._registerProjectToSW(),this._installVisibilityReRegister();try{await i.update()}catch(e){this.logger.debug("SW 업데이트 체크 실패 (무시):",e.message)}const r=await this._getSWVersion(i);r?this.logger.debug("SW 버전:",r):this.logger.warn("[TalkFlow] 서비스 워커가 구버전(v1)입니다. 이미지 미리보기 등 리치 알림 기능을 사용하려면 firebase-messaging-sw.js 를 v2 로 업데이트하세요. 가이드: https://docs.talkflow.ai/push/sw-upgrade"),this._enabled=!0,this.logger.info("푸시 알림 활성화 완료")}async _registerServiceWorker(){try{const e=await navigator.serviceWorker.register(this.serviceWorkerPath,{updateViaCache:"none"});return this.logger.debug("서비스 워커 등록 완료:",this.serviceWorkerPath),e}catch(e){throw new O(L.SW_REGISTER_FAILED,`서비스 워커 등록 실패: ${this.serviceWorkerPath}\n프로젝트 public 폴더에 firebase-messaging-sw.js 파일을 배치하세요.`,e)}}async _registerTokenToServer(e){try{const t=await this._getDeviceId();this.apiClient?.authProvider?.setDeviceId(t),await this.apiClient.post("/api/v1/push/devices",{deviceToken:e,deviceType:"WEB",deviceId:t}),this.logger.info("디바이스 토큰 서버 등록 완료")}catch(e){if(this.logger.error("디바이스 토큰 서버 등록 실패:",e),e instanceof O)throw e;throw new O(L.SERVER_REGISTER_FAILED,"디바이스 토큰 서버 등록 실패: "+(e?.message||e),e)}}async _getDeviceId(){if(this._deviceIdCache)return this._deviceIdCache;try{let e=await V();if(!e&&"undefined"!=typeof localStorage){const t=localStorage.getItem(W);if(t){e=t;if(await q(e)){if(await V()===e)try{localStorage.removeItem(W),this.logger.debug("deviceId migrated from localStorage to IndexedDB")}catch(e){}else this.logger.warn("deviceId IDB read-back 불일치, localStorage 보존")}else this.logger.warn("deviceId IDB 저장 실패 (private mode / quota 등), localStorage 보존")}}if(!e){e=J();await q(e)||this.logger.warn("신규 deviceId IDB 저장 실패, 이번 세션만 유효")}return this._deviceIdCache=e,this.apiClient?.authProvider?.setDeviceId(e),e}catch(e){if(this.logger.warn("IndexedDB deviceId 조회/저장 실패, fallback:",e),!this._deviceIdCache){let e=null;if("undefined"!=typeof localStorage)try{e=localStorage.getItem(W)}catch(e){}this._deviceIdCache=e||J()}return this._deviceIdCache}}_onForegroundMessage(e){}async consumePendingRoom(){return K.consumePendingRoom(this.projectId)}reset(){if(this._foregroundMessageUnsubscribe)try{this._foregroundMessageUnsubscribe()}catch(e){this.logger.debug("포그라운드 푸시 리스너 해제 실패 (무시):",e?.message||e)}this._uninstallVisibilityReRegister(),this._unregisterProjectFromSW(),this._foregroundMessageUnsubscribe=null,this._enablePromise=null,this._messaging=null,this._currentToken=null,this._enabled=!1,this._swRegistration=null,this._projectRegisteredToSW=!1}async _registerProjectToSW(){if(this.projectId)try{const e=(await navigator.serviceWorker.ready).active||this._swRegistration&&this._swRegistration.active;if(!e)return void this.logger.debug("SW active worker 없음 — projectId 등록 스킵");e.postMessage({type:"TALKFLOW_REGISTER_PROJECT",projectId:this.projectId}),this._projectRegisteredToSW=!0}catch(e){this.logger.debug("SW projectId 등록 실패 (무시):",e?.message||e)}}_unregisterProjectFromSW(){if(this._projectRegisteredToSW)try{if("undefined"==typeof navigator||!navigator.serviceWorker)return;const e=navigator.serviceWorker.controller;if(!e)return;e.postMessage({type:"TALKFLOW_UNREGISTER_PROJECT"})}catch(e){this.logger.debug("SW projectId 해제 실패 (무시):",e?.message||e)}}_installVisibilityReRegister(){"undefined"==typeof document||this._visibilityHandler||(this._visibilityHandler=()=>{"visible"===document.visibilityState&&this._enabled&&this.projectId&&this._registerProjectToSW().catch(()=>{})},document.addEventListener("visibilitychange",this._visibilityHandler))}_uninstallVisibilityReRegister(){if("undefined"!=typeof document&&this._visibilityHandler){try{document.removeEventListener("visibilitychange",this._visibilityHandler)}catch(e){}this._visibilityHandler=null}}getToken(){return this._currentToken}isEnabled(){return this._enabled}async getMyDevices(){const e=await this.apiClient.get("/api/v1/push/devices/me");return e&&"object"==typeof e&&"data"in e?e.data:e}async setDeviceEnabled(e,t){await this.apiClient.patch("/api/v1/push/devices/me/enabled",{deviceId:e,enabled:t}),this.logger.info(`디바이스 푸시 enabled=${t}: deviceId=${e}`)}async setCurrentDeviceEnabled(e){const t=await this._getDeviceId();await this.setDeviceEnabled(t,e)}async _getSWVersion(e){try{const t=await navigator.serviceWorker.ready,s=t.waiting||t.installing||t.active||e.waiting||e.installing||e.active;return s?new Promise(e=>{const t=setTimeout(()=>e(null),2e3),i=new MessageChannel;i.port1.onmessage=s=>{clearTimeout(t),e(s.data?.version||null)};try{s.postMessage({type:"TALKFLOW_SW_VERSION"},[i.port2])}catch(s){clearTimeout(t),e(null)}}):null}catch{return null}}setLogLevel(e){this.logger.setLevel(e)}}const z=[["connected","connected"],["disconnected","disconnected"],["reconnecting","reconnecting"],["reconnected","reconnected"],["resumed","resumed"],["error","connectionError"]],B=[["syncStarted","syncStarted"],["syncCompleted","syncCompleted"],["gapTruncated","gapTruncated"],["roomListSynced","roomListSynced"],["message","chatMessage"],["newMessage","newChatMessage"],["messageUpdated","messageUpdated"],["messageDeleted","messageDeleted"],["reactionChanged","reactionChanged"],["linkPreviewAttached","linkPreviewAttached"],["messageTranslated","messageTranslated"],["messageRead","messageRead"],["typing","typing"],["assistantProgress","assistantProgress"],["assistantRoomProgress","assistantRoomProgress"],["summaryUpdate","summaryUpdate"],["memberJoined","memberJoined"],["memberLeft","memberLeft"],["roomSubscribed","roomSubscribed"],["roomUnsubscribed","roomUnsubscribed"],["roomListSubscribed","roomListSubscribed"],["roomListUnsubscribed","roomListUnsubscribed"],["roomListUpdate","roomListUpdate"],["roomListMessage","roomListMessage"],["roomListCreated","roomListCreated"],["roomListJoined","roomListJoined"],["roomListLeft","roomListLeft"],["roomListSelfLeft","roomListSelfLeft"],["roomListKicked","roomListKicked"],["roomListSelfKicked","roomListSelfKicked"],["roomListBanned","roomListBanned"],["roomListSelfBanned","roomListSelfBanned"],["roomListRoomUpdated","roomListRoomUpdated"],["roomListUnreadUpdated","roomListUnreadUpdated"],["retentionCleanup","retentionCleanup"]],X=[["localStreamStarted","localStreamStarted"],["localStreamStopped","localStreamStopped"],["remoteTrack","remoteTrack"],["screenShareStarted","screenShareStarted"],["screenShareEnded","screenShareEnded"],["deviceChange","deviceChange"],["mediaStateChanged","mediaStateChanged"],["callStarted","callStarted"],["callEnded","callEnded"],["callRequested","callRequested"],["callAccepted","callAccepted"],["callRejected","callRejected"],["callCancelled","callCancelled"],["callBusy","callBusy"],["incomingCall","incomingCall"],["incomingCallWhileBusy","incomingCallWhileBusy"],["callInvitation","callInvitation"],["userJoined","userJoined"],["userLeft","userLeft"],["participantLeft","participantLeft"],["participantMediaState","participantMediaState"],["peerConnected","peerConnected"],["peerDisconnected","peerDisconnected"],["peerClosed","peerClosed"],["error","webrtcError"]];function Y(e,t,s){s.forEach(([s,i])=>{e.on(s,e=>{t.emit(i,e)})})}const Z=["getRooms","getRoom","getRoomInfo","setMyRoomLanguage","createOneToOneRoom","createGroupRoom","joinGroupRoom","leaveRoom","updateGroupRoom","inviteToGroupRoom","kickMember","banMember","unbanMember","getBannedMembers","getAvailableGroupRooms","getAllGroupRooms","enterRoom","getMessages","fetchLinkPreview","sendMessage","sendMessageOptimistic","sendTextMessage","sendTextMessageOptimistic","sendReply","sendReplyOptimistic","sendDecisionChoice","uploadFile","sendFileMessage","sendFileMessageOptimistic","classifyFileType","resolveMessageCategory","editMessage","deleteMessage","markAsRead","pinMessage","unpinMessage","toggleReaction","subscribeRoom","unsubscribeRoom","unsubscribeAllRooms","setActiveRoom","clearActiveRoom","getActiveRoom","subscribeRoomList","unsubscribeRoomList","isRoomListSubscribed","getSubscribedRooms","isSubscribed","startTyping","stopTyping","getAssistantProgress","resumeAssistantStreams","getAssistants","getRoomAiMeta","rateAssistantMessage","summarizeWithAssistant","translateWithAssistant","refreshSummary","getSummary","getRoomPmPrompt","upsertRoomPmPrompt","activateRoomPmPrompt","deactivateRoomPmPrompt","getRoomPmPromptVersions","activateRoomPmPromptVersion","previewRoomPmPrompt","createVote","getVotes","getVote","updateVote","deleteVote","closeVote","submitVote","uploadVoteImage"],Q=["initializeIceServers","getTurnCredentials","createCallRoom","getCallRoom","joinCallRoomApi","leaveCallRoomApi","enableIncomingCalls","disableIncomingCalls","isIncomingCallsEnabled","startCall","callUser","acceptCall","rejectCall","cancelCall","endCall","toggleVideo","toggleAudio","setVideoEnabled","setAudioEnabled","startScreenShare","stopScreenShare","getLocalStream","getDevices","switchDevice","startDeviceChangeDetection","stopDeviceChangeDetection","applyVideoConstraints","getVideoSettings","getAudioSettings","isInCall","getCurrentRoom","getParticipants","getMediaState","getConnectionSummary","getStats"];function ee(e,t,s){s.forEach(s=>{e[s]=function(...e){return this[t][s](...e)}})}class te extends i{constructor(e){super(),this._validateOptions(e);const t=e.env||g.environment,s=(e.serverUrl||u(t)).replace(/\/$/,"");this.options={serverUrl:s,env:t,apiKey:e.apiKey,projectId:e.projectId,jwtToken:e.jwtToken||null,sessionMode:e.sessionMode||(y(e.jwtToken||"").sid?"refresh":"legacy"),useSockJS:!1!==e.useSockJS,reconnectDelay:e.reconnectDelay||g.reconnectDelay,maxReconnectDelay:e.maxReconnectDelay||g.maxReconnectDelay,maxReconnectAttempts:e.maxReconnectAttempts||g.maxReconnectAttempts,heartbeatIncoming:e.heartbeatIncoming||g.heartbeatIncoming,heartbeatOutgoing:e.heartbeatOutgoing||g.heartbeatOutgoing,resumeOnVisibility:!1!==e.resumeOnVisibility,syncOnReconnect:!1!==e.syncOnReconnect,assistantStreamReplay:!1!==e.assistantStreamReplay,rewindMillis:void 0!==e.rewindMillis?e.rewindMillis:g.rewindMillis,maxSyncPages:e.maxSyncPages||g.maxSyncPages,syncPageSize:e.syncPageSize||g.syncPageSize,iceServers:e.iceServers||g.iceServers,autoSubscribeRoomList:!1!==e.autoSubscribeRoomList,logLevel:void 0!==e.logLevel?e.logLevel:p.WARN},this.logger=new m(this.options.logLevel,"TalkFlowClient"),this._initialized=!1,this._state=n.DISCONNECTED,this._userId=null,this.options.jwtToken&&(this._userId=v(this.options.jwtToken)),this.apiClient=new _({baseUrl:this.options.serverUrl,apiKey:this.options.apiKey,projectId:this.options.projectId,jwtToken:this.options.jwtToken,logLevel:this.options.logLevel}),this.authSession=null,this._authConnectionRequested=!1,this.connectionManager=null,this._chat=null,this._webrtc=null,this._pushManager=null,this._pushEnablePromise=null,this.options.jwtToken&&this._userId&&this._initializeSubClients(),this._initialized=!0,"refresh"===this.options.sessionMode&&this._enableRefreshSession(),this.logger.info("TalkFlowClient initialized",{userId:this._userId,hasToken:!!this.options.jwtToken})}get chat(){return this._chat}get webrtc(){return this._webrtc}get pushManager(){return this._pushManager}get userId(){return this._userId}_validateOptions(e){if(!e)throw new Error("Options are required");if(!e.apiKey&&"refresh"!==e.sessionMode)throw new Error("apiKey is required");if(!e.projectId&&"refresh"!==e.sessionMode)throw new Error("projectId is required");e.apiKey?.startsWith("sk-")&&console.error("🚨 [TalkFlow] Server Key (sk-) 로 SDK 를 초기화했습니다. SDK 는 Client Key (ck-) 만 사용합니다 — loginUser() 응답의 clientKey 를 그대로 쓰세요.")}_initializeSubClients(){!function(e){if(!e.connectionManager){if(!e.userId)throw new Error("userId is required to initialize sub-clients. Please set JWT token first.");e.connectionManager=new C({serverUrl:e.options.serverUrl,jwtToken:e.options.jwtToken,apiKey:e.options.apiKey,projectId:e.options.projectId,useSockJS:e.options.useSockJS,reconnectDelay:e.options.reconnectDelay,maxReconnectDelay:e.options.maxReconnectDelay,maxReconnectAttempts:e.options.maxReconnectAttempts,heartbeatIncoming:e.options.heartbeatIncoming,heartbeatOutgoing:e.options.heartbeatOutgoing,resumeOnVisibility:e.options.resumeOnVisibility,logLevel:e.options.logLevel}),e.connectionManager.getAccessToken=async({forceRefresh:t=!1}={})=>e.authSession?e.authSession.getAccessToken({forceRefresh:t,rejectedToken:e.connectionManager?.jwtToken}):e.options.jwtToken,e._chat=new k({connectionManager:e.connectionManager,apiClient:e.apiClient,userId:e._userId,syncOnReconnect:e.options.syncOnReconnect,assistantStreamReplay:e.options.assistantStreamReplay,rewindMillis:e.options.rewindMillis,maxSyncPages:e.options.maxSyncPages,syncPageSize:e.options.syncPageSize,logLevel:e.options.logLevel}),e._webrtc=new N({connectionManager:e.connectionManager,apiClient:e.apiClient,userId:e._userId,iceServers:e.options.iceServers,logLevel:e.options.logLevel}),e._setupEventForwarding(),e.logger.debug("Sub-clients initialized")}}(this)}_setupEventForwarding(){var e;(e=this).connectionManager&&(e.connectionManager.on("stateChange",({state:t,prevState:s})=>{e._state=t,e.emit("stateChange",{state:t,prevState:s})}),Y(e.connectionManager,e,z),e.chat&&Y(e.chat,e,B),e.webrtc&&Y(e.webrtc,e,X))}isConnected(){return!!this.connectionManager&&this.connectionManager.isConnected()}_enableRefreshSession(){this.authSession||(this.options.sessionMode="refresh",this.authSession=b.forServer(this.options.serverUrl),this.authSession.attach(this),this.apiClient.authProvider=this.authSession,this._authUnsubscribe=this.authSession.on("authStateChanged",e=>this.emit("authStateChanged",e)),this._authErrorUnsubscribe=this.authSession.on("authError",e=>this.emit("authError",e)),!this.authSession.data&&this.options.jwtToken&&y(this.options.jwtToken).sid&&(this.authSession.data={accessToken:this.options.jwtToken,projectId:this.options.projectId,clientKey:this.options.apiKey,user:{userId:this.userId}},this.authSession._setState("authenticated")),this._authReady=Promise.resolve(),this.authSession.data&&(this._authReady=this._acceptAuthSession(this.authSession.data).catch(e=>{throw this._authReady=Promise.resolve(),e})))}async _acceptAuthSession(e){this.options.projectId&&this.options.projectId!==e.projectId&&this.connectionManager&&await this._clearAuthSession(),this.options.apiKey=e.clientKey,this.options.projectId=e.projectId,this.apiClient.apiKey=e.clientKey,this.apiClient.projectId=e.projectId,await this.setToken(e.accessToken)}async _clearAuthSession(){this._authConnectionRequested=!1,this.options.jwtToken=null,this._userId=null,this.apiClient.setJwtToken(null),await this.disconnect(),this._chat?.destroy(),this._webrtc?.destroy(),await(this.connectionManager?.destroy()),this._chat=null,this._webrtc=null,this.connectionManager=null,this.pushManager?.reset(),this._pushManager=null,this._pushEnablePromise=null,this.emit("loggedOut")}getAuthState(){return this.authSession?.getAuthState()||(1e3*Number(y(this.options.jwtToken||"").exp||0)>Date.now()?"authenticated":"unauthenticated")}isLoggedIn(){return"authenticated"===this.getAuthState()}async getAccessToken(e={}){if(this.authSession)return await this._authReady,this.authSession.getAccessToken(e);if(!this.isLoggedIn()){const e=new Error("Sign in is required");throw e.code="SESSION_REVOKED",e}return this.options.jwtToken}async restoreSession(){return this._enableRefreshSession(),await this._authReady,this.authSession.restoreSession()}static async restoreSession({serverUrl:e}){if(!e)throw new Error("serverUrl is required");return b.forServer(e).restoreSession()}async request(e,t,s={}){if(!t.startsWith("/")||t.startsWith("//"))throw new Error("Use an API-relative path");return await this._authReady,this.apiClient.request(e,t,s)}getState(){return this._state}hasToken(){return!!this.options.jwtToken}static async loginUser({serverUrl:e,email:t,password:s}){if(!e)throw new Error("serverUrl is required");if(!t)throw new Error("email is required");if(!s)throw new Error("password is required");return b.forServer(e).login({email:t,password:s})}async loginUser({email:e,password:t}){if(!e||!t)throw new Error("email and password are required");return this._enableRefreshSession(),await this._authReady,this.authSession.login({email:e,password:t})}async getMyProfile(){this._checkToken();const e=await this.apiClient.get("/api/v1/me");return e&&e.data?e.data:e}async updateMyProfile(e){this._checkToken();const t=await this.apiClient.put("/api/v1/me",e),s=t&&t.data?t.data:t;if(s&&s.accessToken)try{await this.setToken(s.accessToken)}catch(e){this.logger.warn("Profile updated but token refresh failed; keeping the old token",e)}return s}async changeMyPassword({currentPassword:e,newPassword:t}){if(this._checkToken(),this.authSession)return this.authSession.changePassword(()=>this.apiClient.request("PUT","/api/v1/me/password",{body:{currentPassword:e,newPassword:t},skipAuth:!0}));const s=await this.apiClient.put("/api/v1/me/password",{currentPassword:e,newPassword:t}),i=s?.data??s;return i?.accessToken&&i?.refreshToken&&(this._enableRefreshSession(),this.authSession._newMarker(!1),await this.authSession._accept(i)),s}async setPreferredLanguage(e){return this.updateMyProfile({preferredLanguage:e})}static detectBrowserLanguage(){return"undefined"!=typeof navigator&&navigator.language?te.normalizeLanguageCode(navigator.language):null}static normalizeLanguageCode(e){if(!e||"string"!=typeof e)return null;const t=e.trim();if(!t)return null;if("OFF"===t.toUpperCase())return"OFF";const s=t.toLowerCase(),i=s.indexOf("-");if(i<0)return s;const n=s.slice(0,i);return"zh"===n?s:n}static displayText(e,t){const s=te.normalizeLanguageCode(t);return e&&e.translations&&s&&e.translations[s]?e.translations[s]:e?e.content:null}async getCompanyDirectory(){return this._checkToken(),this.apiClient.get("/api/v1/company/directory")}async getUsers(e={}){this._checkToken();const{size:t=50,lastId:s,lastSortValue:i}=e;return this.apiClient.get("/api/v1/users",{size:t,lastId:s,lastSortValue:i})}async searchUsers(e){this._checkToken();const{keyword:t,limit:s=20}=e;return this.apiClient.get("/api/v1/users/search",{keyword:t,limit:s})}_checkToken(){if(!this.options.jwtToken)throw new Error('JWT token is required for this operation. Sign in with TalkFlowClient.loginUser({ serverUrl, email, password }) and pass the returned accessToken via setToken(). See README "로그인".')}getUserId(){return this.userId}isInitialized(){return this._initialized}isReady(){return this._initialized&&!!this.connectionManager}setLogLevel(e){this.options.logLevel=e,this.logger.setLevel(e),this.apiClient.logger?.setLevel(e),this.connectionManager&&this.connectionManager.setLogLevel(e),this.chat&&this.chat.setLogLevel(e),this.webrtc&&this.webrtc.setLogLevel(e)}getStatus(){return{initialized:this._initialized,ready:this.isReady(),hasToken:this.hasToken(),connectionState:this._state,isConnected:this.isConnected(),userId:this.userId,chat:this.chat?{subscribedRooms:this.chat.getSubscribedRooms()}:null,webrtc:this.webrtc?{isInCall:this.webrtc.isInCall(),currentRoom:this.webrtc.getCurrentRoom(),participants:this.webrtc.getParticipants(),mediaState:this.webrtc.getMediaState()}:null}}async destroy(){await this.disconnect(),this._authUnsubscribe?.(),this._authErrorUnsubscribe?.(),this.authSession?.detach(this),this.chat&&this.chat.destroy(),this.webrtc&&this.webrtc.destroy(),this.connectionManager&&await this.connectionManager.destroy(),this.removeAllListeners(),this._initialized=!1,this.logger.info("TalkFlowClient destroyed")}}te.ConnectionState=n,te.ErrorTypes=r,te.LogLevel=p,te.Environment=h,function(e){e.prototype.connect=async function(e,t={}){const{enablePush:s=!1}=t;if(e&&await this.setToken(e),this.authSession){await this._authReady,this._authConnectionRequested=!0;const e=await this.authSession.getAccessToken();await this.setToken(e),this.authSession.schedule()}if(!this.options.jwtToken)throw new Error('JWT token is required. Sign in with TalkFlowClient.loginUser({ serverUrl, email, password }) and pass the returned accessToken to the SDK via the jwtToken option, setToken(), or connect(jwt). See README "로그인".');if(!this.authSession&&A(this.options.jwtToken))throw new Error("JWT token has expired. Please update the token before connecting.");if(this.connectionManager||this._initializeSubClients(),await this.connectionManager.connect(),this.webrtc&&await this.webrtc.enableIncomingCalls(),this.chat&&this.options.autoSubscribeRoomList)try{await this.chat.subscribeRoomList()}catch(e){this.logger.warn("Failed to auto-subscribe room list (non-fatal):",e)}s&&this.enablePushNotifications(),this.logger.info("Connected to server")},e.prototype.disconnect=async function(){this._authConnectionRequested=!1,this.authSession?.schedule(),this.connectionManager&&(this.webrtc&&this.webrtc.isInCall()&&this.webrtc.endCall(),this.webrtc&&this.webrtc.disableIncomingCalls(),this.chat&&(this.chat.unsubscribeAllRooms(),this.chat.unsubscribeRoomList()),await this.connectionManager.disconnect(),this.logger.info("Disconnected from server"))},e.prototype.logout=async function(){if(this.authSession)return this.authSession.logout();try{await this.apiClient.post("/v1/auth/signout"),this.logger.info("Logged out from server")}catch(e){this.logger.warn("Server logout failed (proceeding with local cleanup):",e.message)}await this.disconnect(),this.pushManager&&(this.pushManager.reset(),this._pushEnablePromise=null),this.options.jwtToken=null,this._userId=null,this.apiClient.setJwtToken(null),this.emit("loggedOut")},e.prototype.enablePushNotifications=async function(e={}){if(!this.options.jwtToken){const e=new Error("JWT token is required to enable push notifications.");return this.emit("pushFailed",{reason:"NO_TOKEN",error:e}),{ok:!1,reason:"NO_TOKEN",error:e}}if(!this.pushManager){this._pushManager=new K({apiClient:this.apiClient,projectId:this.options.projectId,firebaseConfig:e.firebaseConfig,vapidKey:e.vapidKey,serviceWorkerPath:e.serviceWorkerPath,logLevel:this.options.logLevel});const t=3e5;this.pushManager._onForegroundMessage=e=>{const s=e.data||{};if(s.projectId&&s.projectId!==this.options.projectId)return void this.logger.debug("다른 프로젝트 푸시 무시:",s.projectId);const i=this.chat?.getActiveRoom();if(i&&i===s.roomId)this.logger.debug("포그라운드 푸시 suppress (현재 방):",s.roomId);else{if(s.messageId){if(this._recentPushMessageIds||(this._recentPushMessageIds=new Map),this._recentPushMessageIds.has(s.messageId))return void this.logger.debug("포그라운드 푸시 dedup:",s.messageId);const e=setTimeout(()=>{this._recentPushMessageIds.delete(s.messageId)},t);this._recentPushMessageIds.set(s.messageId,e)}this.emit("pushNotification",{title:e.notification?.title||s.title,body:e.notification?.body||s.body,data:s})}}}if(this.pushManager.isEnabled())return this.logger.debug("웹 푸시가 이미 활성화되어 있어 중복 호출을 건너뜁니다."),{ok:!0,alreadyEnabled:!0};if(this._pushEnablePromise)return this.logger.debug("웹 푸시 활성화가 이미 진행 중입니다."),this._pushEnablePromise;const t=(async()=>{try{return await this.pushManager.enable(),this.emit("pushEnabled"),{ok:!0}}catch(e){const t=e&&e.code?e.code:"UNKNOWN";return this.logger.warn("Failed to enable push notifications:",t,e?.message),this.emit("pushFailed",{reason:t,error:e}),{ok:!1,reason:t,error:e}}})();return this._pushEnablePromise=t,t.finally(()=>{this._pushEnablePromise===t&&(this._pushEnablePromise=null)}),t},e.prototype.consumePendingRoom=async function(){return K.consumePendingRoom(this.options.projectId)},e.prototype.setCurrentDeviceEnabled=function(e){return this.pushManager?.setCurrentDeviceEnabled(e)},e.prototype.setDeviceEnabled=function(e,t){return this.pushManager?.setDeviceEnabled(e,t)},e.prototype.getMyDevices=function(){return this.pushManager?.getMyDevices()},e.prototype.getPushPermissionState=function(){return K.getPermissionState()},e.prototype.isPushEnabled=function(){return this.pushManager?.isEnabled()??!1},e.prototype.getPushToken=function(){return this.pushManager?.getToken()??null},e.prototype.resetPush=function(){this.pushManager?.reset(),this._pushEnablePromise=null},e.prototype.setToken=async function(e){const{userId:t}=T(e,{validateExpiry:!1}),s=this.userId&&this.userId!==t;this.options.jwtToken=e,this._userId=t,this.apiClient.setJwtToken(e),s&&this.connectionManager?(await this.disconnect(),this._chat?.destroy(),this._webrtc?.destroy(),await this.connectionManager.destroy(),this.pushManager&&(this.pushManager.reset(),this._pushEnablePromise=null,this._pushManager=null),this.connectionManager=null,this._chat=null,this._webrtc=null,this._initializeSubClients()):this.connectionManager?this.connectionManager.updateToken(e):this._initializeSubClients(),this.logger.info("JWT token set",{userId:t}),this.authSession?.replaceAccessToken(e),this.emit("tokenSet",{userId:t})},e.prototype.updateToken=async function(e){await this.setToken(e)}}(te),function(e){ee(e.prototype,"chat",Z),ee(e.prototype,"webrtc",Q)}(te);e.AssistantMode={GENERAL:"GENERAL",PEOPLE_ONLY:"PEOPLE_ONLY",CALL_ONLY:"CALL_ONLY"},e.ChatClient=k,e.ChatMessageType={TEXT:"TEXT",IMAGE:"IMAGE",FILE:"FILE",VIDEO:"VIDEO",AUDIO:"AUDIO",DOCUMENT:"DOCUMENT",MIXED:"MIXED",GALLERY:"GALLERY",SYSTEM:"SYSTEM"},e.ChatRoomType=o,e.ConnectionManager=C,e.ConnectionState=n,e.DecisionCardLimits={MIN_OPTIONS:2,MAX_OPTIONS:5,LABEL_MAX_LENGTH:80,QUESTION_MAX_LENGTH:200},e.DefaultConfig=g,e.Endpoints=d,e.EngagementIntensity={QUIET:"QUIET",NORMAL:"NORMAL",ACTIVE:"ACTIVE"},e.Environment=h,e.ErrorTypes=r,e.EventEmitter=i,e.LogLevel=p,e.Logger=m,e.MediaStreamManager=D,e.PeerConnectionManager=P,e.PersonaRole={LEGAL_ADVISOR:"LEGAL_ADVISOR",MARKETING:"MARKETING",PRODUCT_PLANNING:"PRODUCT_PLANNING",HR:"HR",FINANCE:"FINANCE",CUSTOMER_SUPPORT:"CUSTOMER_SUPPORT",SALES:"SALES",ENGINEERING:"ENGINEERING",DATA_ANALYST:"DATA_ANALYST",PROJECT_MANAGEMENT:"PROJECT_MANAGEMENT",RESEARCH:"RESEARCH",TRANSLATION:"TRANSLATION",DESIGN:"DESIGN",PM:"PM"},e.PmPromptLayerEditorType={SUPER_ADMIN:"SUPER_ADMIN",PROJECT_ADMIN:"PROJECT_ADMIN",ROOM_OWNER:"ROOM_OWNER"},e.PmPromptLayerScope={PROJECT:"PROJECT",ROOM:"ROOM"},e.PushError=O,e.PushErrorCode=L,e.PushManager=K,e.RoomAiType={NONE:"NONE",PERSONA_MULTI:"PERSONA_MULTI",PM_BACKSTAGE:"PM_BACKSTAGE"},e.RoomListEventType=c,e.RoomUnreadReconciler=class{constructor(e){this._currentUserId=e,this._rooms=new Map}_room(e){let t=this._rooms.get(e);return t||(t={snapshotCount:null,snapshotVersion:null,pendingDeltas:new Map},this._rooms.set(e,t)),t}applyMessageReceived(e){if(!e||!e.roomId||!e.messageId)return;if(null!=e.actorId&&e.actorId===this._currentUserId)return;const t=this._room(e.roomId);if(t.pendingDeltas.has(e.messageId))return;const s=null==e.unreadVersion?null:e.unreadVersion;null!=t.snapshotVersion&&null!=s&&s<=t.snapshotVersion||t.pendingDeltas.set(e.messageId,{version:s,delta:1})}applySnapshot(e,t,s){if(!e||null==t)return;const i=null==s?null:s,n=this._room(e);if(!(null!=n.snapshotVersion&&null==i||null!=n.snapshotVersion&&null!=i&&i<n.snapshotVersion||(n.snapshotCount=t,null!=i&&(n.snapshotVersion=i),null==i)))for(const[e,t]of n.pendingDeltas)(null==t.version||t.version<=i)&&n.pendingDeltas.delete(e)}getCount(e){const t=this._rooms.get(e);if(!t||null==t.snapshotCount)return null;let s=t.snapshotCount;for(const e of t.pendingDeltas.values())s+=e.delta;return s}removeRoom(e){this._rooms.delete(e)}clear(){this._rooms.clear()}},e.SenderType={USER:"USER",ASSISTANT:"ASSISTANT"},e.SignalTypes=a,e.SummarizeFormat={MINUTES:"MINUTES",SHORT:"SHORT",TIMELINE:"TIMELINE",ACTIONS:"ACTIONS",OPTIONS:"OPTIONS"},e.WebRTCClient=N,e.WebSocketPaths=l,e.decodeJWTPayload=function(e){try{const t=I(e).split(".");return 3!==t.length?null:JSON.parse(w(t[1]))}catch(e){return console.error("Failed to decode JWT payload:",e),null}},e.default=te,e.extractUserIdFromJWT=v,e.getJWTRemainingTime=function(e){try{const t=I(e).split(".");if(3!==t.length)return-1;const s=JSON.parse(w(t[1]));return s.exp?1e3*s.exp-Date.now():1/0}catch(e){return console.error("Failed to get JWT remaining time:",e),-1}},e.getServerUrl=u,e.isJWTExpired=A,e.validateAndParseJWT=T,Object.defineProperty(e,"__esModule",{value:!0})});
|
|
1
|
+
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("@stomp/stompjs"),require("sockjs-client")):"function"==typeof define&&define.amd?define(["exports","@stomp/stompjs","sockjs-client"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).TalkFlowSDK={},e.StompJs,e.SockJS)}(this,function(e,t,s){"use strict";class i{constructor(){this._events=new Map}on(e,t){return this._events.has(e)||this._events.set(e,new Set),this._events.get(e).add(t),()=>this.off(e,t)}once(e,t){const s=(...i)=>{this.off(e,s),t.apply(this,i)};return s._originalListener=t,this.on(e,s)}off(e,t){const s=this._events.get(e);if(s){for(const e of s)if(e===t||e._originalListener===t){s.delete(e);break}0===s.size&&this._events.delete(e)}}emit(e,t){const s=this._events.get(e);s&&s.forEach(s=>{try{s(t)}catch(t){console.error(`Error in event listener for '${e}':`,t)}})}removeAllListeners(e){e?this._events.delete(e):this._events.clear()}listenerCount(e){const t=this._events.get(e);return t?t.size:0}eventNames(){return Array.from(this._events.keys())}}const n={DISCONNECTED:"disconnected",CONNECTING:"connecting",CONNECTED:"connected",RECONNECTING:"reconnecting",ERROR:"error"},r={CONNECTION_FAILED:"CONNECTION_FAILED",CONNECTION_LOST:"CONNECTION_LOST",CONNECTION_TIMEOUT:"CONNECTION_TIMEOUT",JWT_INVALID:"JWT_INVALID",JWT_EXPIRED:"JWT_EXPIRED",JWT_PARSE_FAILED:"JWT_PARSE_FAILED",UNAUTHORIZED:"UNAUTHORIZED",API_ERROR:"API_ERROR",API_TIMEOUT:"API_TIMEOUT",CHAT_ROOM_NOT_FOUND:"CHAT_ROOM_NOT_FOUND",CHAT_MESSAGE_FAILED:"CHAT_MESSAGE_FAILED",CHAT_SUBSCRIPTION_FAILED:"CHAT_SUBSCRIPTION_FAILED",MEDIA_ACCESS_DENIED:"MEDIA_ACCESS_DENIED",SCREEN_SHARE_DENIED:"SCREEN_SHARE_DENIED",PEER_CONNECTION_FAILED:"PEER_CONNECTION_FAILED",ICE_CONNECTION_FAILED:"ICE_CONNECTION_FAILED",SIGNALING_FAILED:"SIGNALING_FAILED",DEVICE_SWITCH_FAILED:"DEVICE_SWITCH_FAILED",ENUMERATE_DEVICES_FAILED:"ENUMERATE_DEVICES_FAILED",CALL_ROOM_NOT_FOUND:"CALL_ROOM_NOT_FOUND",INVALID_STATE:"INVALID_STATE",UNKNOWN_ERROR:"UNKNOWN_ERROR"},a={CALL_OFFER:"call_offer",CALL_ANSWER:"call_answer",ICE_CANDIDATE:"ice_candidate",JOIN_ROOM:"join_room",LEAVE_ROOM:"leave_room",PEER_JOINED:"peer_joined",PEER_LEFT:"peer_left",VIDEO_STATE_CHANGED:"video_state_changed",AUDIO_STATE_CHANGED:"audio_state_changed",SCREEN_SHARE_STARTED:"screen_share_started",SCREEN_SHARE_ENDED:"screen_share_ended",CALL_REQUEST:"call_request",CALL_ACCEPT:"call_accept",CALL_REJECT:"call_reject",CALL_CANCEL:"call_cancel",CALL_END:"call_end",CALL_INVITATION:"call_invitation",CALL_BUSY:"call_busy"},o={DIRECT:"DIRECT",GROUP:"GROUP",PRIVATE_GROUP:"PRIVATE_GROUP",TEAM:"TEAM"},c={MESSAGE_RECEIVED:"MESSAGE_RECEIVED",MESSAGE_DELETED:"MESSAGE_DELETED",MESSAGE_UPDATED:"MESSAGE_UPDATED",ROOM_CREATED:"ROOM_CREATED",ROOM_JOINED:"ROOM_JOINED",ROOM_LEFT:"ROOM_LEFT",ROOM_UPDATED:"ROOM_UPDATED",ROOM_KICKED:"ROOM_KICKED",ROOM_BANNED:"ROOM_BANNED",ROOM_OWNER_CHANGED:"ROOM_OWNER_CHANGED",ROOM_DELETED:"ROOM_DELETED",MESSAGE_RETENTION_CLEANUP:"MESSAGE_RETENTION_CLEANUP",ROOM_UNREAD_UPDATED:"ROOM_UNREAD_UPDATED"},l={SOCKJS_ENDPOINT:"/ws-chat",NATIVE_ENDPOINT:"/ws-chat-native",APP_PREFIX:"/app",TOPIC_PREFIX:"/topic",QUEUE_PREFIX:"/queue",USER_PREFIX:"/user",getChatDestination:e=>`/topic/chat/${e}`,getChatReadDestination:e=>`/topic/chat/${e}/read`,getChatTypingDestination:e=>`/topic/chat/${e}/typing`,getChatAssistantStreamDestination:e=>`/topic/chat/${e}/assistant-stream`,getChatSummaryDestination:e=>`/topic/chat/${e}/summary`,ROOM_LIST_USER_DESTINATION:"/user/queue/rooms",getWebRTCDestination:e=>`/topic/webrtc/${e}`,getWebRTCUserDestination:()=>"/user/queue/webrtc",CHAT_SEND:"/app/chat/send",CHAT_READ:"/app/chat/read",CHAT_TYPING:"/app/chat/typing",WEBRTC_SIGNAL:"/app/webrtc/signal"},h={DEVELOPMENT:"development",STAGING:"staging",PRODUCTION:"production"},d={[h.DEVELOPMENT]:{serverUrl:"https://dev-chat.apiorbit.net",wsEndpoint:"/ws-chat"},[h.STAGING]:{serverUrl:"https://stg-api.talk-x.app",wsEndpoint:"/ws-chat"},[h.PRODUCTION]:{serverUrl:"https://prod-api.talk-x.app",wsEndpoint:"/ws-chat"}},g={environment:h.PRODUCTION,reconnectDelay:5e3,maxReconnectDelay:3e4,maxReconnectAttempts:10,heartbeatIncoming:1e4,heartbeatOutgoing:1e4,resumeOnVisibility:!0,syncOnReconnect:!0,rewindMillis:3e3,maxSyncPages:5,syncPageSize:100,apiTimeout:3e4,iceServers:[{urls:"stun:stun.l.google.com:19302"},{urls:"stun:stun1.l.google.com:19302"}],logLevel:2};function u(e){return(d[e]||d[h.PRODUCTION]).serverUrl}const p={DEBUG:0,INFO:1,WARN:2,ERROR:3,NONE:4};class m{constructor(e=p.WARN,t="TalkFlow"){this.level=e,this.prefix=t}setLevel(e){this.level=e}setPrefix(e){this.prefix=e}_format(e,...t){return[`[${(new Date).toISOString()}] [${e}] [${this.prefix}]`,...t]}debug(...e){this.level<=p.DEBUG&&console.debug(...this._format("DEBUG",...e))}info(...e){this.level<=p.INFO&&console.info(...this._format("INFO",...e))}warn(...e){this.level<=p.WARN&&console.warn(...this._format("WARN",...e))}error(...e){this.level<=p.ERROR&&console.error(...this._format("ERROR",...e))}}class _{constructor(e){this.baseUrl=e.baseUrl.replace(/\/$/,""),this.apiKey=e.apiKey,this.projectId=e.projectId,this.jwtToken=e.jwtToken,this.timeout=e.timeout||g.apiTimeout,this.logger=new m(e.logLevel,"ApiClient")}setJwtToken(e){this.jwtToken=e}_getHeaders(){const e={"Content-Type":"application/json","X-API-KEY":this.apiKey,"X-PROJECT-ID":this.projectId};return this.jwtToken&&(e.Authorization=this.jwtToken.startsWith("Bearer ")?this.jwtToken:`Bearer ${this.jwtToken}`),e}async request(e,t,s={}){if(!this.authProvider||s.skipAuth)return this._request(e,t,s);this.jwtToken=await this.authProvider.getAccessToken();const i=this.jwtToken,n=this.authProvider.generation;try{return await this._request(e,t,s)}catch(r){if(401===r.status&&["ACCESS_UNAUTHORIZED","TOKEN_INVALID"].includes(r.response?.code)){if(n!==this.authProvider.generation)throw r;this.jwtToken=await this.authProvider.getAccessToken({forceRefresh:!0,rejectedToken:i});const a=this.jwtToken,o=this.authProvider.generation;try{return await this._request(e,t,s)}catch(e){throw await this.authProvider.authenticationFailed(e,{rejectedToken:a,generation:o}),e}}throw await this.authProvider.authenticationFailed(r,{rejectedToken:i,generation:n}),r}}async _request(e,t,s={}){const{body:i,params:n,headers:a}=s;let o=`${this.baseUrl}${t}`;if(n){const e=new URLSearchParams;Object.entries(n).forEach(([t,s])=>{null!=s&&e.append(t,s)});const t=e.toString();t&&(o+=`?${t}`)}const c=new AbortController,l=setTimeout(()=>c.abort(),this.timeout);let h;try{this.logger.debug(`${e} ${o}`),h=await fetch(o,{method:e,headers:{...this._getHeaders(),...a},credentials:this.authProvider?"omit":"same-origin",body:i?JSON.stringify(i):void 0,signal:c.signal})}catch(t){if(clearTimeout(l),"AbortError"===t.name){const e=new Error("Request timeout");throw e.code=r.API_TIMEOUT,e}throw this.logger.error(`API Error: ${e} ${o}`,t),t}clearTimeout(l);const d=h.headers.get("content-type");let g;if(g=d&&d.includes("application/json")?await h.json():await h.text(),!h.ok){const t=new Error(g?.message||`HTTP ${h.status}`);throw t.code=r.API_ERROR,t.status=h.status,t.response=g,this.logger.error(`API Error: ${e} ${o}`,t),t}return this.logger.debug(`Response ${h.status}`),g}get(e,t){return this.request("GET",e,{params:t})}post(e,t,s,i){return this.request("POST",e,{body:t,params:s,headers:i})}put(e,t){return this.request("PUT",e,{body:t})}patch(e,t){return this.request("PATCH",e,{body:t})}delete(e,t){return this.request("DELETE",e,{params:t})}async upload(e,t,s={}){if(!this.authProvider)return this._upload(e,t,s);this.jwtToken=await this.authProvider.getAccessToken();const i=this.jwtToken,n=this.authProvider.generation;try{return await this._upload(e,t,s)}catch(r){if(401===r.status&&["ACCESS_UNAUTHORIZED","TOKEN_INVALID"].includes(r.response?.code)){if(n!==this.authProvider.generation)throw r;this.jwtToken=await this.authProvider.getAccessToken({forceRefresh:!0,rejectedToken:i});const a=this.jwtToken,o=this.authProvider.generation;try{return await this._upload(e,t,s)}catch(e){throw await this.authProvider.authenticationFailed(e,{rejectedToken:a,generation:o}),e}}throw await this.authProvider.authenticationFailed(r,{rejectedToken:i,generation:n}),r}}_upload(e,t,s={}){const{fieldName:i="file",onProgress:n,signal:a,timeout:o=6e5}=s;return new Promise((s,c)=>{const l=`${this.baseUrl}${e}`,h=new XMLHttpRequest,d=new FormData;d.append(i,t),h.open("POST",l),h.timeout=o,h.setRequestHeader("X-API-KEY",this.apiKey),h.setRequestHeader("X-PROJECT-ID",this.projectId),this.jwtToken&&h.setRequestHeader("Authorization",this.jwtToken.startsWith("Bearer ")?this.jwtToken:`Bearer ${this.jwtToken}`),"function"==typeof n&&(h.upload.onprogress=e=>{e.lengthComputable&&n({loaded:e.loaded,total:e.total,percent:Math.round(e.loaded/e.total*100)})});const g=()=>h.abort();if(a){if(a.aborted)return void c(new Error("Upload cancelled"));a.addEventListener("abort",g)}const u=()=>{a&&a.removeEventListener("abort",g)};h.onload=()=>{u();let e=h.responseText;if((h.getResponseHeader("content-type")||"").includes("application/json"))try{e=JSON.parse(h.responseText)}catch{}if(h.status>=200&&h.status<300)this.logger.debug(`Upload ${h.status}:`,e),s(e);else{const t=new Error(e?.message||`HTTP ${h.status}`);t.code=r.API_ERROR,t.status=h.status,t.response=e,this.logger.error(`Upload Error: POST ${l}`,t),c(t)}},h.onerror=()=>{u();const e=new Error("Network error during upload");e.code=r.API_ERROR,this.logger.error(`Upload Network Error: POST ${l}`,e),c(e)},h.ontimeout=()=>{u();const e=new Error("Upload timeout");e.code=r.API_TIMEOUT,this.logger.error(`Upload Timeout: POST ${l}`,e),c(e)},h.onabort=()=>{u(),c(new Error("Upload cancelled"))},this.logger.debug(`POST ${l} (multipart, ${t.size} bytes)`),h.send(d)})}}const S=new Map,E=new Set(["REFRESH_INVALID","REFRESH_REUSED","SESSION_REVOKED","ACCOUNT_BLOCKED","LOGIN_FAILED"]),y=e=>E.has(e?.response?.code||e?.code);function f(e){try{const t=e.replace(/^Bearer\s+/i,"").split(".")[1].replace(/-/g,"+").replace(/_/g,"/");return JSON.parse(atob(t))}catch{return{}}}class w extends i{static forServer(e){const t=e.replace(/\/$/,"");return S.has(t)||S.set(t,new w(t)),S.get(t)}constructor(e){super(),this.serverUrl=e,this.state="checking",this.data=null,this.clients=new Set,this.generation=0,this.refreshPromise=null,this.timer=null,this.peerWaiters=new Map,this.markerKey=`talkflow:auth:${e}`,this.marker=this._readMarker(),this.serverClock=null,this.channel=null,this.storageListener=e=>{if(e.key!==this.markerKey)return;const t=this._readMarker();(t.revision!==this.marker.revision||t.logout)&&(this.marker=t,this.generation++,this._clear(!1).catch(e=>this.emit("authError",e)))},globalThis.addEventListener?.("storage",this.storageListener),"undefined"!=typeof window&&"undefined"!=typeof BroadcastChannel&&(this.channel=new BroadcastChannel(`talkflow:auth:${e}`),this.channel.onmessage=e=>this._receive(e.data),this.channel.unref?.())}_readMarker(){try{const e=globalThis.localStorage?.getItem(this.markerKey);return e?JSON.parse(e):{revision:"initial",logout:!1}}catch{throw this._storageError()}}_storageError(){const e=new Error("Authentication storage is unavailable");return e.code="AUTH_STORAGE_UNAVAILABLE",e}_writeMarker(e){try{if(!globalThis.localStorage)throw this._storageError();globalThis.localStorage.setItem(this.markerKey,JSON.stringify(e)),this.marker=e}catch{throw this._storageError()}}_newMarker(e,t=e){const s=this._readMarker(),i={revision:globalThis.crypto?.randomUUID?.()||String(Date.now())+"-"+Math.random(),logout:e,deviceId:s.deviceId,pending:t,pendingRefreshToken:e&&t?s.refreshToken||s.pendingRefreshToken:void 0};return this._writeMarker(i),i}setDeviceId(e){this._writeMarker({...this._readMarker(),deviceId:e})}_monotonic(){return globalThis.performance?.now?.()??Date.now()}_serverNow(){return this.serverClock?this.serverClock.server+Math.max(this._monotonic()-this.serverClock.mono,Date.now()-this.serverClock.wall):Date.now()}_shareData(){return this.data?{...this.data,refreshToken:void 0,serverTime:this._serverNow()}:null}_publish(e,t,s=this.marker){const{revision:i,logout:n,pending:r}=s;this.channel?.postMessage({type:e,marker:{revision:i,logout:n,pending:r},data:t})}_setState(e,t,s=!1){const i=this.state;this.state=e,(i!==e||t||s)&&this.emit("authStateChanged",{state:e,previous:i,error:t})}getAuthState(){return this.state}isLoggedIn(){return"authenticated"===this.state}_fresh(e=this.data?.accessToken){return 1e3*Number(f(e||"").exp||0)>this._serverNow()+(this.refreshMargin??6e4)}attach(e){this.clients.add(e)}detach(e){this.clients.delete(e),this.schedule()}_enqueue(e){const t=(this.transition||Promise.resolve()).then(e);return this.transition=t.catch(()=>{}),t}_accept(e,t=!0){const s=this.generation,i=this.marker;return this._enqueue(async()=>{if(s!==this.generation)throw this._cancelled();if(!(e?.accessToken&&e?.projectId&&e?.clientKey&&e?.user?.userId)){const e=new Error("Invalid authentication response");throw e.code="AUTH_UNAVAILABLE",e}const n=this.data&&(this.data.user.userId!==e.user.userId||this.data.projectId!==e.projectId);if(t&&e.refreshToken){const t=this._readMarker();if(t.revision!==i.revision||t.logout)throw this._cancelled();this._writeMarker({...t,refreshToken:e.refreshToken,refreshRequestId:void 0,pending:!1,pendingRefreshToken:void 0})}if(Number.isFinite(e.serverTime)&&(this.serverClock={server:e.serverTime+(e._roundTripMillis||0),mono:this._monotonic(),wall:Date.now()}),this.refreshMargin=Math.min(6e4,Math.max(0,1e3*Number(f(e.accessToken).exp)-this._serverNow())/10),this.data={...e,refreshToken:void 0},await Promise.all([...this.clients].map(t=>t._acceptAuthSession(e))),s!==this.generation)throw this._cancelled();return this._setState("authenticated",void 0,n),t&&this._publish("tokens",this._shareData(),this.marker),this._reconnectClients(),this.schedule(),e})}_reconnectClients(){for(const e of this.clients)if(e._authConnectionRequested&&e.isConnected?.()){const t=e.connectionManager;t?._authenticatedToken!==this.data?.accessToken&&t.forceReconnect("access-token-renewed").catch(e=>this.emit("authError",e))}}_clear(e=!0){const t=this.marker;return this._enqueue(async()=>{this.data=null,clearTimeout(this.timer),this.timer=null,this._setState("unauthenticated"),await Promise.all([...this.clients].map(e=>e._clearAuthSession())),e&&this._publish("logout",void 0,t)})}async _receive(e){if(e?.marker)try{const t=this._readMarker();if(e.marker.revision!==t.revision&&t.revision!==this.marker.revision)return;if("logout"===e.type||"login-start"===e.type){if(e.marker.revision!==t.revision)return;return this.marker=e.marker,this.generation++,await this._clear(!1),void("login-start"===e.type&&this._setState("checking"))}if(e.marker.revision!==this._readMarker().revision||e.marker.logout)return;if("request-token"===e.type&&this._fresh())this._publish("peer-token",{requestId:e.data.requestId,session:this._shareData()});else if("peer-token"===e.type)this.peerWaiters.get(e.data.requestId)?.(e.data.session);else if("tokens"===e.type){this.marker.revision!==e.marker.revision&&this.generation++,this.marker=e.marker;const t=f(e.data?.accessToken||""),s=f(this.data?.accessToken||"");if(t.sid===s.sid&&t.exp<s.exp)return;await this._accept(e.data,!1)}}catch(e){this.emit("authError",e)}}async _peerToken(e){if(!this.channel)return null;const t=`${Date.now()}-${Math.random()}`;return new Promise(s=>{const i=i=>{i&&(i.accessToken===e||1e3*Number(f(i.accessToken).exp)<=Number(i.serverTime)+6e4)||(clearTimeout(n),this.peerWaiters.delete(t),s(i))},n=setTimeout(()=>i(null),100);this.peerWaiters.set(t,i),this._publish("request-token",{requestId:t})})}_locked(e){const t="undefined"!=typeof navigator?navigator.locks:null,s=(this.httpQueue||Promise.resolve()).then(()=>t?.request?t.request(`talkflow:auth:${this.serverUrl}`,e):e());return this.httpQueue=s.catch(()=>{}),s}_newRefreshRequestId(){const e=new Uint8Array(32);if(!globalThis.crypto?.getRandomValues){const e=new Error("Secure randomness is unavailable");throw e.code="AUTH_RANDOM_UNAVAILABLE",e}return globalThis.crypto.getRandomValues(e),Array.from(e,e=>e.toString(16).padStart(2,"0")).join("")}async _request(e,t,s={}){const i=this._monotonic(),n=new AbortController,r=setTimeout(()=>n.abort(),15e3);try{const r=await fetch(`${this.serverUrl}${e}`,{method:"POST",credentials:"omit",signal:n.signal,headers:{"Content-Type":"application/json","X-TalkFlow-Session":"refresh",...s},body:void 0===t?void 0:JSON.stringify(t)}),a=r.headers.get("content-type")?.includes("application/json")?await r.json():await r.text();if(!r.ok){const e=new Error(a?.message||`HTTP ${r.status}`);throw e.status=r.status,e.code=a?.code,e.response=a,e}const o=void 0!==a?.data?a.data:a;return o&&"object"==typeof o?Object.defineProperty({...o},"_roundTripMillis",{value:this._monotonic()-i}):o}finally{clearTimeout(r)}}async login({email:e,password:t}){const s=++this.generation;return this._locked(async()=>{const i=this._readMarker();this._writeMarker(i),i.pendingRefreshToken&&await this._request("/v1/auth/signout",{refreshToken:i.pendingRefreshToken,deviceId:i.deviceId});const n=await this._request("/api/v1/auth/login",{email:e,password:t,previousRefreshToken:i.refreshToken});if(!n?.refreshToken){const e=new Error("Server does not support the refresh session contract");throw e.code="AUTH_PROTOCOL_UNSUPPORTED",e}if(s!==this.generation||this._readMarker().revision!==i.revision)throw await this._request("/v1/auth/signout",{refreshToken:n.refreshToken}),this._cancelled();return this._newMarker(!1),this._accept(n)})}async restoreSession(){if(this._readMarker().logout)return this._readMarker().pending?await this.logout():await this._clear(!1),null;if(this._fresh())return this.data;if(!this._readMarker().refreshToken)return await this._clear(!1),null;this.data||this._setState("checking");try{return await this.getAccessToken(),this.data}catch(e){if(y(e))return null;throw e}}async getAccessToken({forceRefresh:e=!1,rejectedToken:t}={}){if(this._readMarker().logout)throw this._loggedOut();if(this._fresh()&&(!e||t&&t!==this.data.accessToken))return this.data.accessToken;if(this.refreshPromise)return this.refreshPromise;const s=this.generation,i=this._readMarker().revision,n=this._locked(async()=>{if(s!==this.generation||i!==this._readMarker().revision)throw this._cancelled();if(this._fresh()&&(!e||t&&t!==this.data.accessToken))return this.data.accessToken;if(this.marker=this._readMarker(),!this.marker.refreshToken)throw this._loggedOut();this._writeMarker(this.marker);const n=await this._peerToken(t);let r=n;if(!r){const e=this._readMarker();if(s!==this.generation||i!==e.revision||e.logout)throw this._cancelled();const t=e.refreshRequestId||this._newRefreshRequestId();this._writeMarker({...e,refreshRequestId:t}),r=await this._request("/api/v1/auth/refresh",{refreshToken:e.refreshToken,requestId:t})}if(s!==this.generation||i!==this._readMarker().revision)throw this._cancelled();return await this._accept(r,!n),r.accessToken}).catch(async e=>{throw s===this.generation&&i===this._readMarker().revision&&(y(e)?await this.authenticationFailed(e):this.data?this.emit("authError",e):this._setState("unavailable",e)),e}).finally(()=>{this.refreshPromise===n&&(this.refreshPromise=null)});return this.refreshPromise=n,n}async logout(){const e=this.data?.accessToken;this.generation++;const t=this._newMarker(!0),s=this._clear(),i=this._locked(async()=>{const s=await this._request("/v1/auth/signout",{refreshToken:t.pendingRefreshToken,deviceId:t.deviceId},e?{Authorization:"Bearer "+e.replace(/^Bearer\s+/i,"")}:{});return this._readMarker().revision===t.revision&&this._writeMarker({...t,pending:!1,pendingRefreshToken:void 0}),s});await Promise.all([s,i]),this._readMarker().revision===t.revision&&this._writeMarker({...t,pending:!1,pendingRefreshToken:void 0})}async changePassword(e){await this.getAccessToken();const t=++this.generation;return this._locked(async()=>{const s=await e();if(t!==this.generation)throw this._cancelled();const i=s?.data??s;return i?.reauthenticationRequired?(this._newMarker(!0,!1),await this._clear(),s):(this._newMarker(!1),await this._accept(i),s)}).catch(async e=>{throw t===this.generation&&await this.authenticationFailed(e),e})}_loggedOut(){const e=new Error("Sign in is required");return e.code="SESSION_REVOKED",e}async authenticationFailed(e,{rejectedToken:t,generation:s}={}){void 0!==s&&s!==this.generation||t&&this.data&&t!==this.data.accessToken||y(e)&&(this.generation++,this._newMarker(!0,!1),await this._clear())}replaceAccessToken(e){if(!this.data||this.data.accessToken===e)return;const t=f(e);t.sid===f(this.data.accessToken).sid&&t.sub===this.data.user.userId&&(this.data={...this.data,accessToken:e,expiresIn:t.exp},this._publish("tokens",this._shareData()),this._reconnectClients(),this.schedule())}_cancelled(){const e=new Error("Authentication operation was superseded");return e.code="AUTH_OPERATION_CANCELLED",e}schedule(e){if(clearTimeout(this.timer),this.timer=null,!this.data||![...this.clients].some(e=>e._authConnectionRequested))return;const t=e??Math.max(1e3,1e3*Number(f(this.data.accessToken).exp||0)-this._serverNow()-(this.refreshMargin??6e4));this.timer=setTimeout(async()=>{try{const e=this.data?.accessToken;await this.getAccessToken({forceRefresh:!0,rejectedToken:e}),this.data&&await Promise.all([...this.clients].filter(e=>e._authConnectionRequested&&!e.isConnected()).map(e=>e.connectionManager.forceReconnect("access-token-renewed"))),this.schedule()}catch(e){y(e)||this.schedule(3e4)}},t),this.timer.unref?.()}destroy(){this.generation++,clearTimeout(this.timer),this.channel?.close(),globalThis.removeEventListener?.("storage",this.storageListener),this.clients.clear(),this.removeAllListeners(),S.get(this.serverUrl)===this&&S.delete(this.serverUrl)}}function I(e){return e&&"string"==typeof e?e.replace(/^Bearer\s+/i,""):""}function b(e){let t=e.replace(/-/g,"+").replace(/_/g,"/");const s=t.length%4;s&&(t+="=".repeat(4-s));const i=atob(t),n=Uint8Array.from(i,e=>e.charCodeAt(0));return(new TextDecoder).decode(n)}function T(e,t={}){const{bufferSeconds:s=30,validateExpiry:i=!0,validateIssuedAt:n=!1}=t;if(!e||"string"!=typeof e){const e=new Error("JWT token is required");throw e.code=r.JWT_INVALID,e}const a=I(e).split(".");if(3!==a.length){const e=new Error("Invalid JWT format: expected 3 parts separated by dots");throw e.code=r.JWT_INVALID,e}let o;try{o=JSON.parse(b(a[1]))}catch(e){const t=new Error("Invalid JWT: failed to decode payload");throw t.code=r.JWT_PARSE_FAILED,t.originalError=e,t}if(i&&o.exp){const e=1e3*o.exp,t=1e3*s,i=Date.now();if(e<i+t){const t=new Error(e<i?"JWT token expired":`JWT token expires within ${s} seconds`);throw t.code=r.JWT_EXPIRED,t.expiredAt=new Date(e).toISOString(),t}}if(n&&o.iat){const e=6e4;if(1e3*o.iat>Date.now()+e){const e=new Error("JWT token issued in the future");throw e.code=r.JWT_INVALID,e}}const c=o.userId||o.sub||o.user_id;if(!c){const e=new Error("Cannot extract userId from JWT: missing userId, sub, or user_id claim");throw e.code=r.JWT_INVALID,e}return{userId:c,payload:o}}function v(e){try{const t=I(e).split(".");if(3!==t.length)return null;const s=JSON.parse(b(t[1]));return s.userId||s.sub||s.user_id||null}catch(e){return console.error("Failed to extract userId from JWT:",e),null}}function R(e,t=0){try{const s=I(e).split(".");if(3!==s.length)return!0;const i=JSON.parse(b(s[1]));if(!i.exp)return!1;const n=1e3*i.exp,r=1e3*t;return n<Date.now()+r}catch(e){return console.error("Failed to check JWT expiration:",e),!0}}const A=()=>"undefined"!=typeof window&&window.SockJS?window.SockJS:s;class C extends i{constructor(e){super(),this.serverUrl=e.serverUrl.replace(/\/$/,""),this.jwtToken=e.jwtToken,this.apiKey=e.apiKey,this.projectId=e.projectId,this.useSockJS=!1!==e.useSockJS,this.reconnectDelay=e.reconnectDelay||g.reconnectDelay,this.maxReconnectDelay=e.maxReconnectDelay||g.maxReconnectDelay,this.maxReconnectAttempts=e.maxReconnectAttempts||g.maxReconnectAttempts,this.heartbeatIncoming=e.heartbeatIncoming||g.heartbeatIncoming,this.heartbeatOutgoing=e.heartbeatOutgoing||g.heartbeatOutgoing,this.resumeOnVisibility=!1!==e.resumeOnVisibility,this.logger=new m(e.logLevel||p.WARN,"ConnectionManager"),this.state=n.DISCONNECTED,this.stompClient=null,this.reconnectAttempts=0,this.subscriptions=new Map,this.pendingSubscriptions=[],this.userId=null,this._intentionalDisconnect=!1,this._tearingDown=!1,this._connectRequested=!1,this._forceReconnectPromise=null,this._lifecycleHandlers=null,this._installLifecycleListeners()}_installLifecycleListeners(){if("undefined"==typeof window||"function"!=typeof window.addEventListener)return;const e=()=>{"undefined"!=typeof document&&"visible"===document.visibilityState&&this._onResume("visibilitychange")},t=()=>this._onResume("pageshow"),s=()=>this._onResume("online");"undefined"!=typeof document&&"function"==typeof document.addEventListener&&document.addEventListener("visibilitychange",e),window.addEventListener("pageshow",t),window.addEventListener("online",s),this._lifecycleHandlers={onVisibilityChange:e,onPageShow:t,onOnline:s}}_removeLifecycleListeners(){if(!this._lifecycleHandlers)return;const{onVisibilityChange:e,onPageShow:t,onOnline:s}=this._lifecycleHandlers;"undefined"!=typeof document&&"function"==typeof document.removeEventListener&&document.removeEventListener("visibilitychange",e),"undefined"!=typeof window&&"function"==typeof window.removeEventListener&&(window.removeEventListener("pageshow",t),window.removeEventListener("online",s)),this._lifecycleHandlers=null}_onResume(e){if(this.resumeOnVisibility&&!this._intentionalDisconnect)if(this._connectRequested){if(this.state!==n.CONNECTING&&!this._forceReconnectPromise){if(this.state===n.CONNECTED)return this._isSocketAlive()?(this.logger.debug(`Resume (${e}): connection alive`),void this.emit("resumed",{reason:e})):(this.logger.warn(`Resume (${e}): zombie socket detected, forcing reconnect`),void this.forceReconnect(`zombie-socket:${e}`));this.logger.info(`Resume (${e}): reviving connection from state=${this.state}`),this.forceReconnect(e)}}else this.logger.debug(`Resume (${e}) ignored — connect() has not been called yet`)}_isSocketAlive(){const e=this.stompClient&&this.stompClient.webSocket;return!!e&&e.readyState===t.StompSocketState.OPEN}async forceReconnect(e){if(!this._intentionalDisconnect&&this.state!==n.CONNECTING)return this._forceReconnectPromise?(this.logger.debug(`Force reconnect already in progress, joining: ${e}`),this._forceReconnectPromise):(this._forceReconnectPromise=this._doForceReconnect(e).finally(()=>{this._forceReconnectPromise=null}),this._forceReconnectPromise)}async _doForceReconnect(e){if(this.logger.info(`Force reconnect: ${e}`),this.stompClient){this._tearingDown=!0;try{await this.stompClient.deactivate()}catch(e){this.logger.warn("Error deactivating stomp client on force reconnect:",e)}finally{this._tearingDown=!1}this.stompClient=null}this._setState(n.DISCONNECTED),this.reconnectAttempts=0;try{await this.connect()}catch(t){this.logger.warn(`Force reconnect failed (${e}):`,t)}}_setState(e){const t=this.state;this.state=e,this.emit("stateChange",{state:e,prevState:t}),this.logger.info(`State changed: ${t} -> ${e}`)}_getWebSocketUrl(){const e=this.useSockJS?l.SOCKJS_ENDPOINT:l.NATIVE_ENDPOINT;return`${this.serverUrl}${e}`}_createStompClient(){const e=this._getWebSocketUrl(),s={beforeConnect:async e=>{if(!this.getAccessToken)return;let t=0;for(;!this._intentionalDisconnect&&(!e||e.active);)try{const t=await this.getAccessToken({forceRefresh:!0===this._authRejected});if(this._intentionalDisconnect||e&&!e.active)return;return this.updateToken(t),void(this._authRejected=!1)}catch(s){if(this.emit("error",{type:r.CONNECTION_FAILED,message:s.message,error:s}),y(s))return this._setState(n.ERROR),this._connectReject?.(s),this._connectResolve=this._connectReject=null,void await((e||this.stompClient)?.deactivate());const i=Math.min(3e4,1e3*2**Math.min(t++,5))*(.8+.4*Math.random());await new Promise(e=>{this._cancelAuthRetry=()=>{clearTimeout(this._authRetryTimer),e()},this._authRetryTimer=setTimeout(e,i),this._authRetryTimer.unref?.()}),this._cancelAuthRetry=null}},connectHeaders:{Authorization:this.jwtToken.startsWith("Bearer ")?this.jwtToken:`Bearer ${this.jwtToken}`,"X-API-KEY":this.apiKey,"X-PROJECT-ID":this.projectId},heartbeatIncoming:this.heartbeatIncoming,heartbeatOutgoing:this.heartbeatOutgoing,reconnectDelay:this.reconnectDelay,reconnectTimeMode:t.ReconnectionTimeMode.EXPONENTIAL,maxReconnectDelay:this.maxReconnectDelay,debug:e=>{this.logger.level<=p.DEBUG&&this.logger.debug("STOMP:",e)},onConnect:e=>this._onConnect(e),onDisconnect:e=>this._onDisconnect(e),onStompError:e=>this._onStompError(e),onWebSocketClose:e=>this._onWebSocketClose(e),onWebSocketError:e=>this._onWebSocketError(e)};if(this.useSockJS){const t=A();s.webSocketFactory=()=>new t(e)}else{const e=this.serverUrl.startsWith("https")?"wss":"ws",t=this.serverUrl.replace(/^https?:\/\//,"");s.brokerURL=`${e}://${t}${l.NATIVE_ENDPOINT}`}return new("undefined"!=typeof window&&window.StompJs&&window.StompJs.Client?window.StompJs.Client:t.Client)(s)}async connect(){if(this.state!==n.CONNECTED){if(this.state!==n.CONNECTING)return this._setState(n.CONNECTING),this.reconnectAttempts=0,this._intentionalDisconnect=!1,this._connectRequested=!0,new Promise((e,t)=>{this._connectResolve=e,this._connectReject=t;try{this.stompClient=this._createStompClient(),this.stompClient.activate()}catch(e){this._setState(n.ERROR),this._drainPendingSubscriptions("Connection activation failed"),t(e)}});this.logger.warn("Connection in progress")}else this.logger.warn("Already connected")}_onConnect(e){this._authenticatedToken=this.jwtToken;const t=this.reconnectAttempts,s=t>0||!0===this._hasConnectedBefore;this._setState(n.CONNECTED),this.reconnectAttempts=0,this._hasConnectedBefore=!0,this.logger.info("Connected to server",e),this._restoreSubscriptions(),this._processPendingSubscriptions(),this.emit("connected",{frame:e}),s&&this.emit("reconnected",{attempts:t}),this._connectResolve&&(this._connectResolve(),this._connectResolve=null,this._connectReject=null)}_onDisconnect(e){this.logger.info("Disconnected from server",e),this._setState(n.DISCONNECTED),this.emit("disconnected",{frame:e})}_onStompError(e){try{const t=JSON.parse(e.body||e.headers?.message||"{}");this._authRejected=["ACCESS_UNAUTHORIZED","SESSION_REVOKED","TOKEN_INVALID"].includes(t.code)}catch{}this.logger.error("STOMP error:",e),this._setState(n.ERROR);const t={type:r.CONNECTION_FAILED,message:e.headers?.message||"STOMP error",frame:e};this._drainPendingSubscriptions(`STOMP error: ${t.message}`),this.emit("error",t),this._connectReject&&(this._connectReject(new Error(t.message)),this._connectResolve=null,this._connectReject=null)}_onWebSocketClose(e){this.stompClient&&1012===e?.code&&(this.stompClient.reconnectDelay=this.reconnectDelay*(.8+.4*Math.random())),4001===e?.code&&(this._authRejected=!0),this.logger.warn("WebSocket closed:",e),this._tearingDown?this.logger.debug("WebSocket closed during intentional teardown — not counting as reconnect"):this.state!==n.DISCONNECTED&&this._handleReconnect()}_onWebSocketError(e){this.logger.error("WebSocket error:",e),this._connectReject&&(this._drainPendingSubscriptions("WebSocket connection failed"),this._connectReject(new Error("WebSocket connection failed")),this._connectResolve=null,this._connectReject=null)}_handleReconnect(){if(this.reconnectAttempts++,this.reconnectAttempts>this.maxReconnectAttempts){if(this.stompClient){try{this.stompClient.deactivate()}catch(e){this.logger.warn("Error deactivating stomp client on max reconnect:",e)}this.stompClient=null}return this._drainPendingSubscriptions("Max reconnection attempts exceeded"),this._setState(n.ERROR),void this.emit("error",{type:r.CONNECTION_LOST,message:"Max reconnection attempts exceeded"})}this._setState(n.RECONNECTING),this.emit("reconnecting",{attempt:this.reconnectAttempts}),this.logger.info(`Reconnecting... attempt ${this.reconnectAttempts}`)}_restoreSubscriptions(){if(0===this.subscriptions.size)return;this.logger.info(`Restoring ${this.subscriptions.size} subscriptions after reconnect`);const e=new Map(this.subscriptions);this.subscriptions.clear(),e.forEach(({callback:e,headers:t},s)=>{this._subscribeInternal(s,e,t),this.logger.debug(`Restored subscription: ${s}`)})}_processPendingSubscriptions(){for(;this.pendingSubscriptions.length>0;){const{destination:e,callback:t,headers:s,resolve:i}=this.pendingSubscriptions.shift(),n=this._subscribeInternal(e,t,s);i&&i(n)}}_subscribeInternal(e,t,s={}){const i=this.stompClient.subscribe(e,e=>{try{const s=JSON.parse(e.body);t(s,e)}catch(s){this.logger.error("Failed to parse message:",s),t(e.body,e)}},s);return this.subscriptions.set(e,{subscription:i,callback:t,headers:s}),this.logger.debug(`Subscribed to: ${e}`),i}subscribe(e,t,s={}){return this.subscriptions.has(e)&&this.unsubscribe(e),this.state!==n.CONNECTED?new Promise((i,n)=>{this.pendingSubscriptions.push({destination:e,callback:t,headers:s,resolve:i,reject:n}),this.logger.debug(`Queued subscription for: ${e}`)}):Promise.resolve(this._subscribeInternal(e,t,s))}unsubscribe(e){const t=this.subscriptions.get(e);t&&(t.subscription.unsubscribe(),this.subscriptions.delete(e),this.logger.debug(`Unsubscribed from: ${e}`))}unsubscribeAll(){this.subscriptions.forEach(({subscription:e},t)=>{e.unsubscribe(),this.logger.debug(`Unsubscribed from: ${t}`)}),this.subscriptions.clear()}send(e,t,s={}){return this.state!==n.CONNECTED?(this.logger.warn("Cannot send message: not connected"),!1):(this.stompClient.publish({destination:e,body:JSON.stringify(t),headers:s}),this.logger.debug(`Sent to ${e}:`,t),!0)}updateToken(e){this.jwtToken=e,this.stompClient&&(this.stompClient.connectHeaders={...this.stompClient.connectHeaders,Authorization:e.startsWith("Bearer ")?e:`Bearer ${e}`})}async disconnect(){this._intentionalDisconnect=!0,this._connectRequested=!1,this._cancelAuthRetry?.(),this._connectReject?.(Object.assign(new Error("Connection cancelled"),{code:"AUTH_OPERATION_CANCELLED"})),this._connectResolve=this._connectReject=null,this._drainPendingSubscriptions("Disconnected before subscription completed"),this.stompClient&&(this.unsubscribeAll(),await this.stompClient.deactivate(),this.stompClient=null),this._setState(n.DISCONNECTED),this.logger.info("Disconnected")}_drainPendingSubscriptions(e){if(0===this.pendingSubscriptions.length)return;const t=this.pendingSubscriptions;this.pendingSubscriptions=[],t.forEach(({destination:t,reject:s})=>{s&&s(new Error(`${e}: ${t}`))}),this.logger.debug(`Drained ${t.length} pending subscriptions: ${e}`)}isConnected(){return this.state===n.CONNECTED}getState(){return this.state}setLogLevel(e){this.logger.setLevel(e)}async destroy(){this._removeLifecycleListeners(),await this.disconnect(),this.removeAllListeners(),this.logger.info("ConnectionManager destroyed")}}const M=new Set(["text/plain","text/markdown","text/x-markdown"]);class k extends i{constructor(e){super(),this.connectionManager=e.connectionManager,this.apiClient=e.apiClient,this.userId=e.userId,this.logger=new m(e.logLevel||p.WARN,"ChatClient"),this.subscribedRooms=new Map,this.roomListSubscribed=!1,this._activeRoomId=null,this._typingTimers=new Map,this._assistantTypingTimers=new Map,this._assistantTypingTimeoutMs=3e4,this._assistantTypingUserId="__assistant__",this._assistantTypingUserName="AI",this._assistantProgress=new Map,this._assistantStreamTombstones=new Map,this._assistantTombstoneTtlMs=12e4,this._assistantTombstoneWarnThreshold=32,this._assistantTombstoneWarned=new Set,this._assistantSlotSeq=0,this._assistantRoomGeneration=new Map,this._assistantSnapshotPending=new Map,this._assistantSnapshotInFlight=new Map,this.assistantStreamReplay=!1!==e.assistantStreamReplay,this._assistantPhaseLabels={THINKING:"생각 중…",SEARCHING:"검색 중…",PLANNING:"기획 중…",DOCUMENT_ANALYSIS:"문서 분석 중…",VIDEO_ANALYSIS:"영상 분석 중…",WRITING:"작성 중…"},this._assistantProgressFallbackTtlMs=35e3,this._seenChatMessageIdsByRoom=new Map,this._seenRoomListMessageIdsByRoom=new Map,this.syncOnReconnect=!1!==e.syncOnReconnect,this.rewindMillis=void 0!==e.rewindMillis?e.rewindMillis:g.rewindMillis,this.maxSyncPages=e.maxSyncPages||g.maxSyncPages,this.syncPageSize=e.syncPageSize||g.syncPageSize,this._anchor=new Map,this._anchorGen=new Map,this._syncInFlight=new Set,this._replaying=!1,this._maxSeenPerRoom=Math.max(200,this.maxSyncPages*this.syncPageSize+100),this._installSyncTriggers()}_installSyncTriggers(){this.connectionManager&&"function"==typeof this.connectionManager.on&&(this._onReconnectedForSync=({attempts:e}={})=>{this.syncAfterReconnect(`reconnected:${e??0}`).catch(e=>this.logger.warn("Gap sync after reconnect failed:",e)),this.resumeAssistantStreams(`reconnected:${e??0}`).catch(e=>this.logger.warn("Assistant stream replay after reconnect failed:",e))},this._onResumedForSync=({reason:e}={})=>{this.syncAfterReconnect(`resumed:${e??"unknown"}`).catch(e=>this.logger.warn("Gap sync after resume failed:",e)),this.resumeAssistantStreams(`resumed:${e??"unknown"}`).catch(e=>this.logger.warn("Assistant stream replay after resume failed:",e))},this.connectionManager.on("reconnected",this._onReconnectedForSync),this.connectionManager.on("resumed",this._onResumedForSync))}_shouldDedupMessage(e,t,s){if(!t||!s)return!1;let i=e.get(t);if(i||(i=new Map,e.set(t,i)),i.has(s))return!0;if(i.set(s,!0),i.size>this._maxSeenPerRoom){const e=i.keys().next().value;i.delete(e)}return!1}async getRooms(e={}){const{size:t=50,lastId:s,lastSortValue:i,type:n}=e;return this.apiClient.get("/api/v1/rooms/my",{size:t,lastId:s,lastSortValue:i,type:n})}async getRoom(e){const t=this._anchorGen.get(e)||0,s=await this.apiClient.get(`/api/v1/rooms/${e}`),i=this._unwrapSuccessResponse(s);return i&&Array.isArray(i.messages)&&this._seedAnchorFromLatest(e,i.messages,t),s}async enterRoom(e){const t=this.subscribedRooms.has(e);if(t){if(this.assistantStreamReplay){const t=this._armAssistantSnapshot(e);this._loadAssistantSnapshot(e,t).catch(t=>this.logger.warn(`Assistant snapshot on re-enter failed: room=${e}`,t))}}else await this.subscribeRoom(e);this.setActiveRoom(e);try{return await this.getRoom(e)}catch(s){throw this.getActiveRoom()===e&&this.clearActiveRoom(),t||this.unsubscribeRoom(e),s}}async getRoomInfo(e){return this.apiClient.get(`/api/v1/rooms/${e}/info`)}async setMyRoomLanguage(e,t){return this.apiClient.put(`/api/v1/rooms/${e}/my-language`,{language:t})}async createOneToOneRoom(e){return this.apiClient.post(`/api/v1/rooms/direct/${e}`)}async createGroupRoom(e){const t=e.roomType||o.GROUP;if(t===o.PRIVATE_GROUP){if(!e.password||e.password.length<4)throw new Error("PRIVATE_GROUP requires a password of at least 4 characters")}else if(t===o.GROUP){if(e.password)throw new Error("Public GROUP rooms cannot have a password")}else if(t===o.TEAM&&e.password)throw new Error("TEAM rooms cannot have a password (invite-only)");return this.apiClient.post("/api/v1/rooms/group",{roomName:e.roomName,description:e.description,invitedUserIds:e.invitedUserIds,roomType:t,password:e.password,messageRetentionHours:e.messageRetentionHours,invitedAssistantPersonaIds:e.invitedAssistantPersonaIds,assistantMode:e.assistantMode,roomAiType:e.roomAiType,engagementIntensity:e.engagementIntensity})}async getAssistants(){const e=await this.apiClient.get("/api/v1/assistants");return this._unwrapSuccessResponse(e)}async getRoomAiMeta(){const e=await this.apiClient.get("/api/v1/rooms/ai-meta");return this._unwrapSuccessResponse(e)}async rateAssistantMessage(e,t,s,i){if(!Number.isInteger(s)||s<1||s>5)throw new Error("rating must be an integer between 1 and 5");await this.apiClient.post(`/api/v1/chat-messages/${e}/messages/${t}/rating`,{rating:s,comment:i})}async summarizeWithAssistant(e,t={}){const{personaId:s,format:i,messageCount:n}=t;if(!s)throw new Error("summarizeWithAssistant requires options.personaId");const r=await this.apiClient.post(`/api/v1/rooms/${e}/assistant/tools/summarize`,{personaId:s,format:i,messageCount:n});return this._unwrapSuccessResponse(r)}async uploadVideoAsset(e,t={}){if(!e)throw new Error("file is required");return this._unwrapSuccessResponse(await this.apiClient.upload("/api/v1/video-assets",e,{onProgress:t.onProgress,signal:t.signal}))}async analyzeVideo(e,t={}){if("string"!=typeof t.idempotencyKey||!t.idempotencyKey.trim())throw new Error("idempotencyKey is required for video analysis");return this._unwrapSuccessResponse(await this.apiClient.post("/api/v1/video-analyses",e,void 0,{"Idempotency-Key":t.idempotencyKey}))}async analyzeVideoWithAssistant(e,t,s={}){if(!e||!t?.personaId||!t?.triggerMessageId)throw new Error("roomId, personaId and triggerMessageId are required");if("string"!=typeof s.idempotencyKey||!s.idempotencyKey.trim())throw new Error("idempotencyKey is required for video analysis");return this._unwrapSuccessResponse(await this.apiClient.post(`/api/v1/rooms/${encodeURIComponent(e)}/assistant/tools/analyze-video`,t,void 0,{"Idempotency-Key":s.idempotencyKey}))}async getVideoAnalysis(e){if(!e)throw new Error("jobId is required");return this._unwrapSuccessResponse(await this.apiClient.get(`/api/v1/video-analyses/${encodeURIComponent(e)}`))}async cancelVideoAnalysis(e){if(!e)throw new Error("jobId is required");return this._unwrapSuccessResponse(await this.apiClient.post(`/api/v1/video-analyses/${encodeURIComponent(e)}/cancel`))}async getVideoAnalysisCapabilities(){return this._unwrapSuccessResponse(await this.apiClient.get("/api/v1/video-analyses/capabilities"))}async translateWithAssistant(e,t={}){const{targetLang:s,sourceMessageId:i}=t;if(!i)throw new Error("translateWithAssistant requires options.sourceMessageId");if(!s)throw new Error("translateWithAssistant requires options.targetLang");const n=await this.apiClient.post(`/api/v1/rooms/${e}/assistant/tools/translate`,{targetLang:s,sourceMessageId:i});return this._unwrapSuccessResponse(n)}async refreshSummary(e,t={}){const{format:s,lang:i}=t,n=await this.apiClient.post(`/api/v1/rooms/${e}/summary/refresh`,{format:s,lang:i});return this._unwrapSuccessResponse(n)}async getSummary(e,t={}){const{format:s,lang:i,afterSeq:n,size:r}=t,a=await this.apiClient.get(`/api/v1/rooms/${e}/summary`,{format:s,lang:i,afterSeq:n,size:r});return this._unwrapSuccessResponse(a)}async getRoomPmPrompt(e){const t=await this.apiClient.get(`/api/v1/rooms/${e}/pm-prompt`);return this._unwrapSuccessResponse(t)}async upsertRoomPmPrompt(e,t){if("string"!=typeof t||!t.trim())throw new Error("upsertRoomPmPrompt requires non-blank content");if(t.length>2e4)throw new Error("upsertRoomPmPrompt content exceeds 20000 characters");const s=await this.apiClient.put(`/api/v1/rooms/${e}/pm-prompt`,{content:t});return this._unwrapSuccessResponse(s)}async activateRoomPmPrompt(e){const t=await this.apiClient.patch(`/api/v1/rooms/${e}/pm-prompt/activate`);return this._unwrapSuccessResponse(t)}async deactivateRoomPmPrompt(e){const t=await this.apiClient.patch(`/api/v1/rooms/${e}/pm-prompt/deactivate`);return this._unwrapSuccessResponse(t)}async getRoomPmPromptVersions(e){const t=await this.apiClient.get(`/api/v1/rooms/${e}/pm-prompt/versions`);return this._unwrapSuccessResponse(t)}async activateRoomPmPromptVersion(e,t){if(!Number.isInteger(t)||t<1)throw new Error("activateRoomPmPromptVersion requires a positive integer version");const s=await this.apiClient.post(`/api/v1/rooms/${e}/pm-prompt/versions/${t}/activate`);return this._unwrapSuccessResponse(s)}async previewRoomPmPrompt(e){const t=await this.apiClient.get(`/api/v1/rooms/${e}/pm-prompt/preview`);return this._unwrapSuccessResponse(t)}async joinGroupRoom(e,t){const s=t?{password:t}:void 0;return this.apiClient.post(`/api/v1/rooms/group/${e}/join`,s)}async leaveRoom(e,t={}){const s=t.newOwnerId?{newOwnerId:t.newOwnerId}:void 0,i=await this.apiClient.post(`/api/v1/rooms/${e}/leave`,s);return this.unsubscribeRoom(e),i}async transferOwnership(e,t){const s=await this.apiClient.put(`/api/v1/rooms/${e}/owner`,{newOwnerId:t});return s&&"object"==typeof s&&"data"in s?s.data:s}async deleteRoom(e){const t=await this.apiClient.delete(`/api/v1/rooms/${e}`);return this.unsubscribeRoom(e),t}async updateGroupRoom(e,t){return this.apiClient.put(`/api/v1/rooms/group/${e}`,t)}async inviteToGroupRoom(e,t){const s=Array.isArray(t)?{userIds:t}:{userIds:t?.userIds,assistantPersonaIds:t?.assistantPersonaIds};return this.apiClient.post(`/api/v1/rooms/group/${e}/invite`,s)}async kickMember(e,t){return this.apiClient.delete(`/api/v1/rooms/${e}/members/${t}`)}async banMember(e,t){return this.apiClient.post(`/api/v1/rooms/${e}/banned-members`,{userId:t})}async unbanMember(e,t){return this.apiClient.delete(`/api/v1/rooms/${e}/banned-members/${t}`)}async getBannedMembers(e){const t=await this.apiClient.get(`/api/v1/rooms/${e}/banned-members`);return t&&"object"==typeof t&&"data"in t?t.data:t}async pinMessage(e,t){return this.apiClient.post(`/api/v1/rooms/group/${e}/pin/${t}`)}async unpinMessage(e){return this.apiClient.post(`/api/v1/rooms/group/${e}/unpin`)}async createVote(e,t,s={}){if(!e)throw new Error("roomId is required");const{idempotencyKey:i}=s;if(!i)throw new Error("createVote: options.idempotencyKey is required — generate it once per vote and reuse the same value on retry (재시도에 같은 키를 넘겨야 투표가 중복 생성되지 않는다).");const n=await this.apiClient.post(`/api/v1/rooms/${e}/votes`,t,void 0,{"X-Idempotency-Key":i});return this._unwrapSuccessResponse(n)}async getVotes(e,t={}){const{cursor:s,limit:i}=t,n=await this.apiClient.get(`/api/v1/rooms/${e}/votes`,{cursor:s,limit:i});return this._unwrapSuccessResponse(n)}async getVote(e,t){const s=await this.apiClient.get(`/api/v1/rooms/${e}/votes/${t}`);return this._unwrapSuccessResponse(s)}async updateVote(e,t,s){const i=await this.apiClient.patch(`/api/v1/rooms/${e}/votes/${t}`,s);return this._unwrapSuccessResponse(i)}async deleteVote(e,t){await this.apiClient.delete(`/api/v1/rooms/${e}/votes/${t}`)}async closeVote(e,t){await this.apiClient.patch(`/api/v1/rooms/${e}/votes/${t}/close`)}async submitVote(e,t,s){const i=await this.apiClient.put(`/api/v1/rooms/${e}/votes/${t}/responses`,{optionIds:s});return this._unwrapSuccessResponse(i)}async uploadVoteImage(e,t,s={}){if(!e)throw new Error("roomId is required");if(!t)throw new Error("file is required");const i=await this.apiClient.upload(`/api/v1/rooms/${e}/votes/images`,t,{onProgress:s.onProgress,signal:s.signal});return this._unwrapSuccessResponse(i)}async toggleReaction(e,t,s){return this.apiClient.post(`/api/v1/chat-messages/${e}/messages/${t}/reaction`,{emoji:s})}async getAvailableGroupRooms(e={}){const{size:t=50,lastId:s,lastSortValue:i}=e;return this.apiClient.get("/api/v1/rooms/groups/available",{size:t,lastId:s,lastSortValue:i})}async getAllGroupRooms(e={}){const{size:t=50,lastId:s,lastSortValue:i}=e;return this.apiClient.get("/api/v1/rooms/groups/all",{size:t,lastId:s,lastSortValue:i})}async getMessages(e,t={}){const{size:s=50,lastId:i,lastSortValue:n}=t,r=null==i&&null==n,a=this._anchorGen.get(e)||0,o=await this.apiClient.get(`/api/v1/chat-messages/${e}/list`,{size:s,lastId:i,lastSortValue:n}),c=this._unwrapSuccessResponse(o);return c&&Array.isArray(c.content)&&(c.content.forEach(e=>this._normalizeMessageTimestamps(e)),r&&this._seedAnchorFromLatest(e,c.content,a)),o}_seedAnchorFromLatest(e,t,s){if(!Array.isArray(t)||0===t.length)return!1;const i=t[0];return!(!i||!i.id)&&this._commitAnchorFromSync(e,i.id,s)}async syncMessages(e,t={}){const{afterId:s,rewindMillis:i,size:n=this.syncPageSize}=t,r=await this.apiClient.get(`/api/v1/chat-messages/${e}/sync`,{afterId:s,rewindMillis:i,size:n}),a=this._unwrapSuccessResponse(r);return a&&Array.isArray(a.content)&&a.content.forEach(e=>this._normalizeMessageTimestamps(e)),r}_advanceAnchorFromLive(e,t){e&&t&&(this._anchor.set(e,t),this._anchorGen.set(e,(this._anchorGen.get(e)||0)+1))}_commitAnchorFromSync(e,t,s){return!!t&&((this._anchorGen.get(e)||0)!==s?(this.logger.debug(`Anchor commit skipped (live advanced): room=${e}`),!1):(this._anchor.set(e,t),!0))}_replayMessages(e,t){if(!Array.isArray(t)||0===t.length)return 0;let s=0;this._replaying=!0;try{t.forEach(t=>{this._handleChatMessage(e,{...t,eventType:t.eventType||"MESSAGE_CREATED"})&&(s+=1)})}finally{this._replaying=!1}return s}async _reloadLatestPage(e,t){const s=await this.getMessages(e,{size:this.syncPageSize}),i=this._unwrapSuccessResponse(s)||{},n=(Array.isArray(i.content)?i.content:[]).slice().reverse(),r=this._replayMessages(e,n),a=n.length?n[n.length-1].id:null;return this._commitAnchorFromSync(e,a,t),r}_isCursorExpired(e){return 410===e?.status||"SYNC_CURSOR_EXPIRED"===e?.response?.code}async _syncRoom(e){const t=this._anchor.get(e);if(!t)return null;if(this._syncInFlight.has(e))return null;this._syncInFlight.add(e);const s=this._anchorGen.get(e)||0,i=Date.now();try{const n=[];let r=t,a=this.rewindMillis,o=!0,c=0;for(;o&&c<this.maxSyncPages;){const t=await this.syncMessages(e,{afterId:r,rewindMillis:a}),s=this._unwrapSuccessResponse(t)||{},i=Array.isArray(s.content)?s.content:[];n.push(...i),a=0,o=!!s.hasNext,s.lastId&&(r=s.lastId),c+=1}if(o){this.logger.info(`Gap too large, falling back to latest page: room=${e}`),this.emit("gapTruncated",{roomId:e,fetched:n.length});return{roomId:e,recovered:await this._reloadLatestPage(e,s),truncated:!0,durationMs:Date.now()-i}}const l=this._replayMessages(e,n);return this._commitAnchorFromSync(e,r,s),{roomId:e,recovered:l,truncated:!1,durationMs:Date.now()-i}}catch(t){if(this._isCursorExpired(t)){this.logger.warn(`Sync cursor expired, reloading latest: room=${e}`),this.emit("gapTruncated",{roomId:e,fetched:0,reason:"cursorExpired"});return{roomId:e,recovered:await this._reloadLatestPage(e,s),truncated:!0,durationMs:Date.now()-i}}throw t}finally{this._syncInFlight.delete(e)}}async syncAfterReconnect(e="manual"){if(!this.syncOnReconnect)return;const t=Array.from(this.subscribedRooms.keys()).filter(e=>this._anchor.has(e));if(0===t.length)return void await this._refreshRoomList(e);this.emit("syncStarted",{roomIds:t,reason:e});const s=Date.now(),i=[];for(const e of t)try{const t=await this._syncRoom(e);t&&i.push(t)}catch(t){this.logger.warn(`Gap sync failed: room=${e}`,t)}await this._refreshRoomList(e),this.emit("syncCompleted",{rooms:i,totalRecovered:i.reduce((e,t)=>e+(t.recovered||0),0),durationMs:Date.now()-s,reason:e})}async _refreshRoomList(e){if(this.roomListSubscribed)try{const t=await this.getRooms({size:50}),s=this._unwrapSuccessResponse(t)||{};this.emit("roomListSynced",{rooms:s.content||[],reason:e})}catch(e){this.logger.warn("Room list refresh failed (non-fatal):",e)}}async fetchLinkPreview(e){const t="string"==typeof e?e.trim():"";if(!t)throw new Error("url is required");const s=await this.apiClient.post("/api/v1/link-preview",{url:t});return this._unwrapSuccessResponse(s)}async sendMessage(e,t){const s=this._generateMessageId();return this._sendMessageWithId(e,s,t)}sendMessageOptimistic(e,t){const s=this._generateMessageId(),i=[s],n=this._sendMessageWithId(e,s,t).then(e=>this._attachOptimisticMetadata(i,e));return{baseMessageId:s,messageIds:i,promise:n}}async _sendMessageWithId(e,t,s){this.stopTyping(e);const i=await this.apiClient.post(`/api/v1/chat-messages/${e}/send`,{messageId:t,message:s.message,fileInfos:s.fileInfos,replyToMessageId:s.replyToMessageId,decisionResponse:s.decisionResponse}),n=this._unwrapSuccessResponse(i);return n&&Array.isArray(n.messages)&&n.messages.forEach(e=>this._normalizeMessageTimestamps(e)),n}async sendTextMessage(e,t){return this.sendMessage(e,{message:t})}sendTextMessageOptimistic(e,t){return this.sendMessageOptimistic(e,{message:t})}async uploadFile(e,t,s={}){if(!e)throw new Error("roomId is required");if(!t)throw new Error("file is required");const i=await this.apiClient.upload(`/api/v1/files/${e}/upload`,t,{onProgress:s.onProgress,signal:s.signal});return this._unwrapSuccessResponse(i)}async sendFileMessage(e,t,s={}){const i=this._normalizeFileArray(t),n=await this._uploadFiles(e,i,s),r=this._applyFileMetadata(n,s.metadata);return this.sendMessage(e,{message:s.message,fileInfos:r,replyToMessageId:s.replyToMessageId})}sendFileMessageOptimistic(e,t,s={}){const i=this._normalizeFileArray(t),n=this._generateMessageId(),r=[n],a=(async()=>{const t=await this._uploadFiles(e,i,s),a=this._applyFileMetadata(t,s.metadata),o=await this._sendMessageWithId(e,n,{message:s.message,fileInfos:a,replyToMessageId:s.replyToMessageId});return this._attachOptimisticMetadata(r,o)})();return{baseMessageId:n,messageIds:r,promise:a}}_normalizeFileArray(e){const t=Array.isArray(e)?e:[e];if(0===t.length)throw new Error("At least one file is required");if(t.length>20)throw new Error("A maximum of 20 files can be sent in one message");return t}async _uploadFiles(e,t,s={}){return Promise.all(t.map((t,i)=>this.uploadFile(e,t,{signal:s.signal,onProgress:s.onUploadProgress?e=>s.onUploadProgress({...e,fileIndex:i}):void 0})))}_applyFileMetadata(e,t){return null==t?e:e.map((e,s)=>{const i=Array.isArray(t)?t[s]:t;return null==i?e:{...e,metadata:i}})}async sendReply(e,t,s){return this.sendMessage(e,{message:t,replyToMessageId:s})}sendReplyOptimistic(e,t,s){return this.sendMessageOptimistic(e,{message:t,replyToMessageId:s})}async sendDecisionChoice(e,t,s){if(!t||!t.messageId)throw new Error("sendDecisionChoice: card with messageId is required");if(!s||!s.id)throw new Error("sendDecisionChoice: option with id is required");return this.sendMessage(e,{message:s.label,replyToMessageId:t.messageId,decisionResponse:{optionId:s.id}})}async deleteMessage(e,t,s="ALL"){return this.apiClient.post(`/api/v1/chat-messages/${e}/messages/${t}/delete`,{deleteType:s})}async editMessage(e,t,s){return this.apiClient.put(`/api/v1/chat-messages/${e}/messages/${t}`,{message:s})}_hasTextMessage(e){return"string"==typeof e&&e.trim().length>0}_baseMimeType(e){const t=e||"",s=t.indexOf(";");return(s>=0?t.slice(0,s):t).trim().toLowerCase()}classifyFileType(e){const t=this._baseMimeType(e);return t.startsWith("image/")?"IMAGE":t.startsWith("video/")?"VIDEO":t.startsWith("audio/")?"AUDIO":t.includes("pdf")||t.includes("document")||t.includes("msword")||t.includes("ms-excel")||t.includes("ms-powerpoint")||M.has(t)?"DOCUMENT":"FILE"}_classifyFileType(e){return this.classifyFileType(e)}resolveMessageCategory(e){const t=e&&e.chatMessageType,s=e&&e.fileMetaDataList||[];if("GALLERY"!==t)return t||"TEXT";if(0===s.length)return"FILE";const i=new Set(s.map(e=>this.classifyFileType(e&&e.fileType||"")));return 1===i.size?i.values().next().value:"MIXED"}_attachOptimisticMetadata(e,t){const s=t&&Array.isArray(t.messages)?t.messages.map(e=>e&&e.messageId||null):[],i=s.length===e.length&&e.every((e,t)=>e===s[t]);return i||this.logger.warn("Optimistic messageId prediction mismatch — server may have changed grouping rules. Use result.optimistic.actualMessageIds to reconcile.",{predicted:e,actual:s}),{...t,optimistic:{predictedMessageIds:e,actualMessageIds:s,predictionMatched:i}}}_generateMessageId(){return"undefined"!=typeof crypto&&"function"==typeof crypto.randomUUID?crypto.randomUUID():"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,e=>{const t=16*Math.random()|0;return("x"===e?t:3&t|8).toString(16)})}_unwrapSuccessResponse(e){return e&&"object"==typeof e&&Object.prototype.hasOwnProperty.call(e,"data")?e.data:e}_normalizeMessageTimestamps(e){return e&&"object"==typeof e?(e.sentAt=this._toEpochMillis(e.sentAt),e.editedAt=this._toEpochMillis(e.editedAt),e.replyTo&&"object"==typeof e.replyTo&&(e.replyTo.sentAt=this._toEpochMillis(e.replyTo.sentAt)),e):e}_toEpochMillis(e){if("string"!=typeof e)return e;const t=Date.parse(e);return Number.isNaN(t)?e:t}async subscribeRoom(e){if(this.subscribedRooms.has(e))return void this.logger.warn(`Already subscribed to room: ${e}`);const t=l.getChatDestination(e);await this.connectionManager.subscribe(t,t=>{this._handleChatMessage(e,t)});const s=l.getChatReadDestination(e);await this.connectionManager.subscribe(s,t=>{this._handleReadEvent(e,t)});const i=l.getChatTypingDestination(e);await this.connectionManager.subscribe(i,t=>{this._handleTypingEvent(e,t)});const n=this.assistantStreamReplay?this._armAssistantSnapshot(e):null,r=l.getChatAssistantStreamDestination(e);await this.connectionManager.subscribe(r,t=>{this._handleAssistantStreamEvent(e,t)}),null!==n&&this._loadAssistantSnapshot(e,n).catch(t=>this.logger.warn(`Assistant snapshot merge failed: room=${e}`,t));const a=l.getChatSummaryDestination(e);await this.connectionManager.subscribe(a,t=>{this._handleSummaryEvent(e,t)});const o=t=>{t.roomId===e&&this.emit("memberJoined",{roomId:e,members:t.members||[],participantCount:t.activeParticipantCount,timestamp:t.timestamp})},c=t=>{t.roomId===e&&this.emit("memberLeft",{roomId:e,members:t.members||[],participantCount:t.activeParticipantCount,timestamp:t.timestamp})};this.on("roomListJoined",o),this.on("roomListLeft",c),this.subscribedRooms.set(e,{chatDestination:t,readDestination:s,typingDestination:i,assistantStreamDestination:r,summaryDestination:a,memberJoinedHandler:o,memberLeftHandler:c,subscribedAt:new Date}),this.logger.info(`Subscribed to room: ${e}`),this.emit("roomSubscribed",{roomId:e})}unsubscribeRoom(e){const t=this.subscribedRooms.get(e);t?(this.connectionManager.unsubscribe(t.chatDestination),this.connectionManager.unsubscribe(t.readDestination),t.typingDestination&&this.connectionManager.unsubscribe(t.typingDestination),t.assistantStreamDestination&&this.connectionManager.unsubscribe(t.assistantStreamDestination),t.summaryDestination&&this.connectionManager.unsubscribe(t.summaryDestination),t.memberJoinedHandler&&this.off("roomListJoined",t.memberJoinedHandler),t.memberLeftHandler&&this.off("roomListLeft",t.memberLeftHandler),this._clearTypingTimer(e),this._clearAssistantTypingTimer(e),this._clearAssistantProgress(e),this._activeRoomId===e&&(this._activeRoomId=null),this.subscribedRooms.delete(e),this._seenChatMessageIdsByRoom.delete(e),this._seenRoomListMessageIdsByRoom.delete(e),this.logger.info(`Unsubscribed from room: ${e}`),this.emit("roomUnsubscribed",{roomId:e})):this.logger.warn(`Not subscribed to room: ${e}`)}unsubscribeAllRooms(){this.subscribedRooms.forEach((e,t)=>{this.unsubscribeRoom(t)})}setActiveRoom(e){this._activeRoomId=e,this.logger.debug(`Active room set: ${e}`)}clearActiveRoom(){this.logger.debug(`Active room cleared (was: ${this._activeRoomId})`),this._activeRoomId=null}getActiveRoom(){return this._activeRoomId}async subscribeRoomList(){if(this.roomListSubscribed)return void this.logger.warn("Already subscribed to room list");const e=l.ROOM_LIST_USER_DESTINATION;await this.connectionManager.subscribe(e,e=>{this._handleRoomListEvent(e)}),this.roomListSubscribed=!0,this.logger.info("Subscribed to room list"),this.emit("roomListSubscribed",{})}unsubscribeRoomList(){this.roomListSubscribed?(this.connectionManager.unsubscribe(l.ROOM_LIST_USER_DESTINATION),this.roomListSubscribed=!1,this.logger.info("Unsubscribed from room list"),this.emit("roomListUnsubscribed",{})):this.logger.warn("Not subscribed to room list")}isRoomListSubscribed(){return this.roomListSubscribed}markAsRead(e,t){return this.connectionManager.isConnected()?this.connectionManager.send(l.CHAT_READ,{roomId:e,messageId:t}):(this.logger.warn("Cannot mark as read: not connected"),!1)}_handleChatMessage(e,t){if(this.logger.debug(`Message received in room ${e} (${t.eventType}):`,t),"MESSAGE_CREATED"===t.eventType&&this._shouldDedupMessage(this._seenChatMessageIdsByRoom,e,t.messageId))return this.logger.debug(`Duplicate MESSAGE_CREATED suppressed: room=${e}, messageId=${t.messageId}`),!1;switch("MESSAGE_CREATED"!==t.eventType||this._replaying||this._advanceAnchorFromLive(e,t.id),"MESSAGE_UPDATED"===t.eventType&&(void 0===t.translations&&(t.translations=null),void 0===t.sourceLang&&(t.sourceLang=null),void 0===t.translationsOf&&(t.translationsOf=null)),this.emit("message",{roomId:e,message:t}),t.eventType){case"MESSAGE_CREATED":"ASSISTANT"===t.senderType&&(this._clearAssistantTypingTimer(e),this.emit("typing",{roomId:e,userId:this._assistantTypingUserId,userName:this._assistantTypingUserName,typing:!1,senderType:"ASSISTANT"})),t.userId!==this.userId&&(this.emit("newMessage",{roomId:e,message:t}),this._activeRoomId===e&&t.messageId&&this.markAsRead(e,t.messageId));break;case"MESSAGE_UPDATED":this.emit("messageUpdated",{roomId:e,message:t});break;case"MESSAGE_DELETED":this.emit("messageDeleted",{roomId:e,message:t});break;case"REACTION_CHANGED":this.emit("reactionChanged",{roomId:e,message:t});break;case"LINK_PREVIEW_ATTACHED":this.emit("linkPreviewAttached",{roomId:e,message:t});break;case"MESSAGE_TRANSLATED":this.emit("messageTranslated",{roomId:e,message:t});break;default:this.logger.warn(`Unknown eventType "${t.eventType}" — falling back to legacy behavior`),t.userId!==this.userId&&(this.emit("newMessage",{roomId:e,message:t}),this._activeRoomId===e&&t.messageId&&this.markAsRead(e,t.messageId))}return!0}_handleRoomListEvent(e){if(this.logger.debug("Room list event:",e),e.eventType===c.MESSAGE_RECEIVED&&e.messageId&&this._shouldDedupMessage(this._seenRoomListMessageIdsByRoom,e.roomId,e.messageId))this.logger.debug(`Duplicate roomList MESSAGE_RECEIVED suppressed: room=${e.roomId}, messageId=${e.messageId}`);else switch(this.emit("roomListUpdate",e),e.eventType){case c.MESSAGE_RECEIVED:this.emit("roomListMessage",e);break;case c.MESSAGE_DELETED:this.emit("roomListMessageDeleted",e);break;case c.MESSAGE_UPDATED:this.emit("roomListMessageUpdated",e);break;case c.ROOM_CREATED:this.emit("roomListCreated",e);break;case c.ROOM_JOINED:this.emit("roomListJoined",e);break;case c.ROOM_LEFT:this.emit("roomListLeft",e),e.actorId===this.userId&&this.emit("roomListSelfLeft",e);break;case c.ROOM_KICKED:this.emit("roomListKicked",e),this._isSelfInMembers(e)&&this.emit("roomListSelfKicked",e);break;case c.ROOM_OWNER_CHANGED:this.emit("roomListOwnerChanged",e);break;case c.ROOM_DELETED:this.subscribedRooms.has(e.roomId)&&this.unsubscribeRoom(e.roomId),this.emit("roomListDeleted",e);break;case c.ROOM_BANNED:this.emit("roomListBanned",e),this._isSelfInMembers(e)&&this.emit("roomListSelfBanned",e);break;case c.ROOM_UPDATED:this.emit("roomListRoomUpdated",e);break;case c.MESSAGE_RETENTION_CLEANUP:this.emit("retentionCleanup",{roomId:e.roomId,cutoffTime:e.cutoffTime});break;case c.ROOM_UNREAD_UPDATED:this.emit("roomListUnreadUpdated",{roomId:e.roomId,roomUnreadCount:e.roomUnreadCount,unreadVersion:e.unreadVersion});break;default:this.logger.warn("Unknown room list event type:",e.eventType)}}_handleReadEvent(e,t){this.logger.debug(`Read event in room ${e}:`,t);const s=t.roomId||e;t.events&&Array.isArray(t.events)?t.events.forEach(e=>{this.emit("messageRead",{roomId:s,messageId:e.messageId,userId:t.userId,remainingUnreadCount:e.remainingUnreadCount})}):this.emit("messageRead",{roomId:s,messageId:t.messageId,userId:t.userId,remainingUnreadCount:t.remainingUnreadCount})}startTyping(e){if(!this.connectionManager.isConnected())return;const t=this._typingTimers.get(e);t?clearTimeout(t):this.connectionManager.send(l.CHAT_TYPING,{roomId:e,typing:!0});const s=setTimeout(()=>{this.stopTyping(e)},3e3);this._typingTimers.set(e,s)}stopTyping(e){this._clearTypingTimer(e),this.connectionManager.isConnected()&&this.connectionManager.send(l.CHAT_TYPING,{roomId:e,typing:!1})}_isSelfInMembers(e){return Array.isArray(e.members)&&e.members.some(e=>e&&e.userId===this.userId)}_handleTypingEvent(e,t){"ASSISTANT"!==t.senderType&&t.userId===this.userId||("ASSISTANT"===t.senderType&&!0===t.typing&&this._startAssistantTypingTimer(e),this.emit("typing",{roomId:e,userId:t.userId,userName:t.userName,typing:t.typing,senderType:t.senderType||"USER"}))}_startAssistantTypingTimer(e){this._clearAssistantTypingTimer(e);const t=setTimeout(()=>{this._assistantTypingTimers.delete(e),this.emit("typing",{roomId:e,userId:this._assistantTypingUserId,userName:this._assistantTypingUserName,typing:!1,senderType:"ASSISTANT"}),this.logger.debug(`Assistant typing auto-cleared (timeout): room=${e}`)},this._assistantTypingTimeoutMs);this._assistantTypingTimers.set(e,t)}_clearAssistantTypingTimer(e){const t=this._assistantTypingTimers.get(e);t&&(clearTimeout(t),this._assistantTypingTimers.delete(e))}_clearTypingTimer(e){const t=this._typingTimers.get(e);t&&(clearTimeout(t),this._typingTimers.delete(e))}_handleAssistantStreamEvent(e,t){if(!t||!t.type)return;const s=t.streamId||null;if(!s)return void this.logger.debug(`Assistant stream event without streamId ignored: room=${e}, type=${t.type}`);if(this._isAssistantStreamClosed(e,s))return void this.logger.debug(`Assistant event for closed stream ignored: room=${e}, stream=${s}, type=${t.type}`);const i=this._assistantSnapshotPending.get(e);if(i&&"DONE"!==t.type)i.events.push(t);else{if("PHASE"===t.type){const i=this._assistantSlot(e,s),n="number"==typeof t.createdAt?t.createdAt:null;if(i&&null!==n&&n<=(i.phaseUpdatedAt||0))return void this.logger.debug(`Stale assistant PHASE ignored: room=${e}, stream=${s}, createdAt=${n}, applied=${i.phaseUpdatedAt}`);const r=t.phase||null,a=Boolean(i&&i.phase===r),o=t.progress?this._normalizeAssistantProgress(t.progress):a&&i?i.progress:null,c=i,l=this._setAssistantSlot(e,s,t,{phase:r,progress:o,phaseUpdatedAt:null!==n?n:c?c.phaseUpdatedAt:0,text:c&&c.text||"",lastSeq:c?this._seqOf(c.lastSeq):-1,textAvailable:!c||!1!==c.textAvailable});return void this._emitAssistantStream(e,s,l,{active:!0,text:l.textAvailable&&l.text||null})}if("DELTA"===t.type){const i=this._assistantSlot(e,s),n=i&&i.phase||"WRITING",r="number"==typeof t.seq?t.seq:null;let a=i?this._seqOf(i.lastSeq):-1,o=!i||!1!==i.textAvailable,c=i&&i.text||"";if(null===r)o&&(c+=t.delta||"");else{if(r<=a)return void this.logger.debug(`Duplicate assistant DELTA ignored: room=${e}, stream=${s}, seq=${r}, lastSeq=${a}`);r===a+1?(o&&(c+=t.delta||""),a=r):(this.logger.debug(`Assistant DELTA gap — text dropped: room=${e}, stream=${s}, seq=${r}, lastSeq=${a}`),o=!1,c="",a=r)}const l=this._setAssistantSlot(e,s,t,{phase:n,progress:i&&i.progress||null,text:c,lastSeq:a,textAvailable:o});return void this._emitAssistantStream(e,s,l,{active:!0,text:l.textAvailable?l.text:null,delta:l.textAvailable&&t.delta||null,seq:r})}if("DONE"===t.type){const i=this._assistantSlot(e,s);i&&i.timer&&clearTimeout(i.timer),this._deleteAssistantSlot(e,s),this._tombstoneAssistantStream(e,s),this._emitAssistantStream(e,s,i,{active:!1,personaId:t.personaId||i&&i.personaId||null,phase:null,progress:null,label:null,status:t.status||null,messageId:t.messageId||null})}}}async _loadAssistantSnapshot(e,t){const s=this._assistantSnapshotInFlight.get(e);if(s&&s.generation===t)return s.task;const i=(async()=>{let s=null;try{const t=await this.apiClient.get(`/api/v1/rooms/${e}/assistant/streams`),i=this._unwrapSuccessResponse(t);s=i&&Array.isArray(i.streams)?i.streams:[]}catch(t){this.logger.warn(`Assistant stream snapshot failed (live-only fallback): room=${e}`,t)}if(this._assistantGeneration(e)!==t)return this.logger.debug(`Assistant snapshot discarded (stale generation): room=${e}`),void this._deleteAssistantPendingIfGeneration(e,t);s&&this._applyAssistantSnapshot(e,s),this._drainAssistantPending(e,t)})();this._assistantSnapshotInFlight.set(e,{generation:t,task:i});try{await i}finally{const s=this._assistantSnapshotInFlight.get(e);s&&s.generation===t&&this._assistantSnapshotInFlight.delete(e)}}_deleteAssistantPendingIfGeneration(e,t){const s=this._assistantSnapshotPending.get(e);s&&s.generation===t&&this._assistantSnapshotPending.delete(e)}_applyAssistantSnapshot(e,t){t.forEach(t=>{const s=t&&t.streamId;if(!s)return;if(this._isAssistantStreamClosed(e,s))return;if("DONE"===t.status)return this._tombstoneAssistantStream(e,s),void this._emitAssistantStream(e,s,null,{active:!1,phase:null,progress:null,label:null,personaId:t.personaId||null,status:t.doneStatus||null,messageId:t.messageId||null,resumed:!0});const i=!1!==t.textAvailable,n=this._setAssistantSlot(e,s,{personaId:t.personaId,expiresAt:void 0},{phase:t.phase||null,progress:this._normalizeAssistantProgress(t.progress),phaseUpdatedAt:"number"==typeof t.phaseUpdatedAt?t.phaseUpdatedAt:0,text:i&&t.text||"",lastSeq:this._seqOf(t.lastSeq),textAvailable:i,resumed:!0});this._emitAssistantStream(e,s,n,{active:!0,text:n.textAvailable&&n.text||null,resumed:!0})})}_drainAssistantPending(e,t){const s=this._assistantSnapshotPending.get(e);s&&(void 0!==t&&s.generation!==t||(this._assistantSnapshotPending.delete(e),s.events.forEach(t=>this._handleAssistantStreamEvent(e,t))))}_assistantGeneration(e){return this._assistantRoomGeneration.get(e)||0}_armAssistantSnapshot(e){const t=this._assistantGeneration(e)+1;this._assistantRoomGeneration.set(e,t);const s=this._assistantSnapshotPending.get(e);return this._assistantSnapshotPending.set(e,{generation:t,events:s?s.events:[]}),t}async resumeAssistantStreams(e="manual"){if(!this.assistantStreamReplay)return;const t=Array.from(this.subscribedRooms.keys());0!==t.length&&(this.logger.debug(`Assistant stream replay: rooms=${t.length}, reason=${e}`),await Promise.all(t.map(e=>{const t=this._armAssistantSnapshot(e);return this._loadAssistantSnapshot(e,t).catch(t=>this.logger.warn(`Assistant stream replay failed: room=${e}`,t))})))}_assistantSlots(e){let t=this._assistantProgress.get(e);return t||(t=new Map,this._assistantProgress.set(e,t)),t}_assistantSlot(e,t){const s=this._assistantProgress.get(e);return s?s.get(t):void 0}_deleteAssistantSlot(e,t){const s=this._assistantProgress.get(e);s&&(s.delete(t),0===s.size&&this._assistantProgress.delete(e))}_isAssistantStreamClosed(e,t){const s=this._assistantStreamTombstones.get(e);if(!s)return!1;const i=s.get(t);return void 0!==i&&(!(i<=Date.now())||(s.delete(t),!1))}_tombstoneAssistantStream(e,t){let s=this._assistantStreamTombstones.get(e);s||(s=new Map,this._assistantStreamTombstones.set(e,s));const i=Date.now();s.forEach((e,t)=>{e<=i&&s.delete(t)}),s.set(t,i+this._assistantTombstoneTtlMs),s.size>this._assistantTombstoneWarnThreshold?this._assistantTombstoneWarned.has(e)||(this._assistantTombstoneWarned.add(e),this.logger.warn(`Assistant tombstones above threshold — NOT pruning (TTL only): room=${e}, size=${s.size}, threshold=${this._assistantTombstoneWarnThreshold}`)):this._assistantTombstoneWarned.delete(e)}_handleSummaryEvent(e,t){t&&t.status&&this.emit("summaryUpdate",{roomId:e,status:t.status,format:t.format||null,languages:t.languages||null,updatedAt:t.updatedAt||null})}_seqOf(e){return"number"==typeof e?e:-1}_setAssistantSlot(e,t,s,i){const n=this._assistantSlot(e,t);n&&n.timer&&clearTimeout(n.timer);let r=this._assistantProgressFallbackTtlMs;"number"==typeof s.expiresAt&&(r=Math.min(12e4,Math.max(5e3,s.expiresAt-Date.now())));const a=setTimeout(()=>{this._expireAssistantStream(e,t)},r),o={personaId:s.personaId||n&&n.personaId||null,phase:i.phase,progress:i.progress||null,phaseUpdatedAt:void 0!==i.phaseUpdatedAt?i.phaseUpdatedAt:n&&n.phaseUpdatedAt||0,text:i.text||"",lastSeq:this._seqOf(i.lastSeq),textAvailable:!1!==i.textAvailable,resumed:!0===i.resumed||!!n&&!0===n.resumed,updatedAt:Date.now(),order:++this._assistantSlotSeq,timer:a};return this._assistantSlots(e).set(t,o),o}_expireAssistantStream(e,t){const s=this._assistantSlot(e,t);s&&(s.timer&&clearTimeout(s.timer),this._deleteAssistantSlot(e,t),this.logger.debug(`Assistant progress auto-cleared (timeout): room=${e}, stream=${t}`),this._emitAssistantStream(e,t,s,{active:!1,phase:null,progress:null,label:null,reason:"timeout"}))}_emitAssistantStream(e,t,s,i){const n="phase"in i?i.phase:s?s.phase:null,r="progress"in i?i.progress:s?s.progress:null;this.emit("assistantProgress",{roomId:e,streamId:t,personaId:s?s.personaId:null,phase:n,progress:r,label:this._assistantPhaseLabel(n,r),textAvailable:!s||s.textAvailable,lastSeq:s?s.lastSeq:-1,...i}),this._emitAssistantRoomProgress(e)}_emitAssistantRoomProgress(e){const{streams:t,...s}=this._assistantRoomProgressPayload(e);this.emit("assistantRoomProgress",s)}_assistantRoomProgressPayload(e){const t=this._assistantProgress.get(e),s=t?Array.from(t.entries()):[];if(0===s.length)return{roomId:e,active:!1,streamCount:0,streamId:null,personaId:null,phase:null,label:null,text:null,textAvailable:!0,progress:null,streams:[]};const[i,n]=s.reduce((e,t)=>t[1].order>e[1].order?t:e);return{roomId:e,active:!0,streamCount:s.length,streamId:i,personaId:n.personaId,phase:n.phase,progress:n.progress||null,label:this._assistantPhaseLabel(n.phase,n.progress),text:n.textAvailable&&n.text||null,textAvailable:n.textAvailable,streams:s.sort((e,t)=>e[1].order-t[1].order).map(([e,t])=>({streamId:e,personaId:t.personaId,phase:t.phase,progress:t.progress||null,label:this._assistantPhaseLabel(t.phase,t.progress),text:t.textAvailable&&t.text||null,textAvailable:t.textAvailable,resumed:!0===t.resumed}))}}_normalizeAssistantProgress(e){if(!e||!["ANALYZING","SUMMARIZING"].includes(e.stage))return null;if("SUMMARIZING"===e.stage)return{stage:"SUMMARIZING",current:null,total:null,unit:null};const t=Number.isInteger(e.current)?e.current:null,s=Number.isInteger(e.total)?e.total:null,i=["DOCUMENT","PAGE","TEXT_CHUNK"].includes(e.unit)?e.unit:null;return null===t||null===s||!i||t<0||s<1||t>s?null:{stage:"ANALYZING",current:t,total:s,unit:i}}_assistantPhaseLabel(e,t){if(!e)return null;if("DOCUMENT_ANALYSIS"!==e)return this._assistantPhaseLabels[e]||null;if(t&&"SUMMARIZING"===t.stage)return"분석 결과 종합 중…";if(!t||"ANALYZING"!==t.stage)return this._assistantPhaseLabels[e];const s="PAGE"===t.unit?"페이지":"TEXT_CHUNK"===t.unit?"구간":"개";return`문서 분석 중… (${t.current}/${t.total}${s})`}getAssistantProgress(e){return this._assistantRoomProgressPayload(e)}_clearAssistantProgress(e){const t=this._assistantProgress.get(e);t&&(t.forEach(e=>e.timer&&clearTimeout(e.timer)),this._assistantProgress.delete(e)),this._assistantStreamTombstones.delete(e),this._assistantTombstoneWarned.delete(e),this._assistantSnapshotPending.delete(e),this._assistantRoomGeneration.set(e,(this._assistantRoomGeneration.get(e)||0)+1)}getSubscribedRooms(){return Array.from(this.subscribedRooms.keys())}isSubscribed(e){return this.subscribedRooms.has(e)}setLogLevel(e){this.logger.setLevel(e)}destroy(){this.unsubscribeAllRooms(),this.roomListSubscribed&&this.unsubscribeRoomList(),this._typingTimers.forEach(e=>clearTimeout(e)),this._typingTimers.clear(),this._assistantTypingTimers.forEach(e=>clearTimeout(e)),this._assistantTypingTimers.clear(),this._assistantProgress.forEach(e=>e.forEach(e=>e.timer&&clearTimeout(e.timer))),this._assistantProgress.clear(),this._assistantStreamTombstones.clear(),this._assistantTombstoneWarned.clear(),this._assistantRoomGeneration.clear(),this._assistantSnapshotPending.clear(),this._assistantSnapshotInFlight.clear(),this._seenChatMessageIdsByRoom.clear(),this._seenRoomListMessageIdsByRoom.clear(),this.connectionManager&&"function"==typeof this.connectionManager.off&&(this._onReconnectedForSync&&this.connectionManager.off("reconnected",this._onReconnectedForSync),this._onResumedForSync&&this.connectionManager.off("resumed",this._onResumedForSync)),this._anchor.clear(),this._anchorGen.clear(),this._syncInFlight.clear(),this.removeAllListeners(),this.logger.info("ChatClient destroyed")}}class D extends i{constructor(e={}){super(),this.localStream=null,this.screenStream=null,this.videoEnabled=!0,this.audioEnabled=!0,this.logger=new m(e.logLevel||p.WARN,"MediaStreamManager"),this._deviceChangeHandler=null}async getUserMedia(e={video:!0,audio:!0}){try{return this.localStream=await navigator.mediaDevices.getUserMedia(e),this.videoEnabled=!1!==e.video,this.audioEnabled=!1!==e.audio,this.emit("streamStarted",{stream:this.localStream}),this.logger.info("User media stream started"),this.localStream}catch(e){throw this.logger.error("Failed to get user media:",e),this.emit("error",{type:r.MEDIA_ACCESS_DENIED,message:this._getMediaErrorMessage(e),error:e}),e}}_getMediaErrorMessage(e){switch(e.name){case"NotAllowedError":return"Camera/Microphone permission denied";case"NotFoundError":return"Camera/Microphone not found";case"NotReadableError":return"Camera/Microphone is already in use";case"OverconstrainedError":return"Camera/Microphone constraints cannot be satisfied";case"AbortError":return"Media access aborted";default:return e.message||"Unknown media error"}}async getDisplayMedia(e={video:!0,audio:!1}){try{return this.screenStream=await navigator.mediaDevices.getDisplayMedia(e),this.screenStream.getVideoTracks()[0].onended=()=>{this.emit("screenShareEnded",{}),this.screenStream=null},this.emit("screenShareStarted",{stream:this.screenStream}),this.logger.info("Screen share stream started"),this.screenStream}catch(e){throw this.logger.error("Failed to get display media:",e),this.emit("error",{type:r.SCREEN_SHARE_DENIED,message:"NotAllowedError"===e.name?"Screen share permission denied":e.message,error:e}),e}}toggleVideo(){if(!this.localStream)return this.logger.warn("No local stream available"),this.videoEnabled;const e=this.localStream.getVideoTracks();return 0===e.length?(this.logger.warn("No video track available"),this.videoEnabled):(this.videoEnabled=!this.videoEnabled,e.forEach(e=>{e.enabled=this.videoEnabled}),this.emit("videoToggled",{enabled:this.videoEnabled}),this.logger.debug(`Video toggled: ${this.videoEnabled}`),this.videoEnabled)}toggleAudio(){if(!this.localStream)return this.logger.warn("No local stream available"),this.audioEnabled;const e=this.localStream.getAudioTracks();return 0===e.length?(this.logger.warn("No audio track available"),this.audioEnabled):(this.audioEnabled=!this.audioEnabled,e.forEach(e=>{e.enabled=this.audioEnabled}),this.emit("audioToggled",{enabled:this.audioEnabled}),this.logger.debug(`Audio toggled: ${this.audioEnabled}`),this.audioEnabled)}setVideoEnabled(e){if(!this.localStream)return void this.logger.warn("No local stream available");this.localStream.getVideoTracks().forEach(t=>{t.enabled=e}),this.videoEnabled=e,this.emit("videoToggled",{enabled:e})}setAudioEnabled(e){if(!this.localStream)return void this.logger.warn("No local stream available");this.localStream.getAudioTracks().forEach(t=>{t.enabled=e}),this.audioEnabled=e,this.emit("audioToggled",{enabled:e})}getLocalStream(){return this.localStream}getScreenStream(){return this.screenStream}isVideoEnabled(){return this.videoEnabled}isAudioEnabled(){return this.audioEnabled}stopAll(){this.localStream&&(this.localStream.getTracks().forEach(e=>e.stop()),this.localStream=null,this.emit("streamStopped",{}),this.logger.info("All media tracks stopped")),this.screenStream&&(this.screenStream.getTracks().forEach(e=>e.stop()),this.screenStream=null)}replaceTrack(e,t){this.localStream?(this.localStream.removeTrack(e),this.localStream.addTrack(t),e.stop(),this.emit("trackReplaced",{oldTrack:e,newTrack:t}),this.logger.debug(`Track replaced: ${e.kind}`)):this.logger.warn("No local stream available")}getTrack(e){if(!this.localStream)return null;const t="video"===e?this.localStream.getVideoTracks():this.localStream.getAudioTracks();return t.length>0?t[0]:null}async getDevices(){try{const e=await navigator.mediaDevices.enumerateDevices();return{videoInputs:e.filter(e=>"videoinput"===e.kind),audioInputs:e.filter(e=>"audioinput"===e.kind),audioOutputs:e.filter(e=>"audiooutput"===e.kind)}}catch(e){throw this.logger.error("Failed to enumerate devices:",e),this.emit("error",{type:r.ENUMERATE_DEVICES_FAILED,message:e.message,error:e}),e}}async switchDevice(e,t){if(!this.localStream)throw new Error("No local stream available");try{const s="video"===t?{video:{deviceId:{exact:e}},audio:!1}:{video:!1,audio:{deviceId:{exact:e}}},i=(await navigator.mediaDevices.getUserMedia(s)).getTracks()[0],n="video"===t?this.localStream.getVideoTracks()[0]:this.localStream.getAudioTracks()[0];return n&&(this.replaceTrack(n,i),this.emit("deviceSwitched",{kind:t,deviceId:e,newTrack:i}),this.logger.info(`${t} device switched to ${e}`)),i}catch(e){throw this.logger.error(`Failed to switch ${t} device:`,e),this.emit("error",{type:r.DEVICE_SWITCH_FAILED,message:e.message,error:e}),e}}startDeviceChangeDetection(){this._deviceChangeHandler||(this._deviceChangeHandler=async()=>{try{const e=await this.getDevices();this.emit("deviceChange",e),this.logger.debug("Device change detected")}catch(e){this.logger.warn("Failed to get devices on change:",e)}},navigator.mediaDevices.addEventListener("devicechange",this._deviceChangeHandler),this.logger.debug("Device change detection started"))}stopDeviceChangeDetection(){this._deviceChangeHandler&&(navigator.mediaDevices.removeEventListener("devicechange",this._deviceChangeHandler),this._deviceChangeHandler=null,this.logger.debug("Device change detection stopped"))}getStreamInfo(){return this.localStream?{hasStream:!0,videoEnabled:this.videoEnabled,audioEnabled:this.audioEnabled,tracks:this.localStream.getTracks().map(e=>({kind:e.kind,id:e.id,label:e.label,enabled:e.enabled,readyState:e.readyState,muted:e.muted}))}:{hasStream:!1,videoEnabled:this.videoEnabled,audioEnabled:this.audioEnabled,tracks:[]}}async applyVideoConstraints(e){if(!this.localStream)throw new Error("No local stream available");const t=this.localStream.getVideoTracks()[0];if(!t)throw new Error("No video track available");try{await t.applyConstraints(e),this.emit("videoConstraintsApplied",{constraints:e}),this.logger.info("Video constraints applied:",e)}catch(e){throw this.logger.error("Failed to apply video constraints:",e),e}}getVideoSettings(){if(!this.localStream)return null;const e=this.localStream.getVideoTracks()[0];return e?e.getSettings():null}getAudioSettings(){if(!this.localStream)return null;const e=this.localStream.getAudioTracks()[0];return e?e.getSettings():null}setLogLevel(e){this.logger.setLevel(e)}destroy(){this.stopAll(),this.stopDeviceChangeDetection(),this.removeAllListeners(),this.logger.info("MediaStreamManager destroyed")}}class L extends i{constructor(e={}){super(),this.iceServers=e.iceServers||g.iceServers,this.logger=new m(e.logLevel||p.WARN,"PeerConnectionManager"),this.peers=new Map,this.localStream=null}setLocalStream(e){this.localStream=e,this.peers.forEach(e=>{this._addTracksToConnection(e.connection)})}setIceServers(e){this.iceServers=e}createPeerConnection(e,t=!1,s={}){if(this.peers.has(e))return this.logger.warn(`Peer connection already exists: ${e}`),this.peers.get(e).connection;const i={iceServers:this.iceServers,iceCandidatePoolSize:10},n=new RTCPeerConnection(i),r={connection:n,polite:t,makingOffer:!1,ignoreOffer:!1,isSettingRemoteAnswerPending:!1,skipAutoNegotiation:s.skipAutoNegotiation||!1};return this.peers.set(e,r),this._setupConnectionHandlers(e,n,r),this.localStream&&this._addTracksToConnection(n),this.logger.info(`Peer connection created: ${e} (polite: ${t}, skipAutoNegotiation: ${r.skipAutoNegotiation})`),n}_setupConnectionHandlers(e,t,s){t.onicecandidate=t=>{t.candidate&&this.emit("iceCandidate",{peerId:e,candidate:t.candidate})},t.oniceconnectionstatechange=()=>{this.logger.debug(`ICE connection state (${e}): ${t.iceConnectionState}`),this.emit("iceConnectionStateChange",{peerId:e,state:t.iceConnectionState}),"failed"===t.iceConnectionState&&this.emit("error",{type:r.ICE_CONNECTION_FAILED,peerId:e,message:"ICE connection failed"})},t.onconnectionstatechange=()=>{this.logger.debug(`Connection state (${e}): ${t.connectionState}`),this.emit("connectionStateChange",{peerId:e,state:t.connectionState}),"connected"===t.connectionState?this.emit("peerConnected",{peerId:e}):"disconnected"!==t.connectionState&&"failed"!==t.connectionState||this.emit("peerDisconnected",{peerId:e})},t.onnegotiationneeded=async()=>{if(s.skipAutoNegotiation)this.logger.debug(`Skipping auto negotiation for ${e} (manual offer will be sent)`);else try{s.makingOffer=!0,await t.setLocalDescription(),this.emit("negotiationNeeded",{peerId:e,description:t.localDescription})}catch(t){this.logger.error(`Negotiation error (${e}):`,t)}finally{s.makingOffer=!1}},t.ontrack=t=>{this.logger.info(`Remote track received from ${e}:`,t.track.kind),this.emit("remoteTrack",{peerId:e,track:t.track,streams:t.streams})},t.ondatachannel=t=>{this.logger.info(`Data channel received from ${e}`),this.emit("dataChannel",{peerId:e,channel:t.channel})}}_addTracksToConnection(e){if(!this.localStream)return;const t=e.getSenders();this.localStream.getTracks().forEach(s=>{t.find(e=>e.track&&e.track.kind===s.kind)||e.addTrack(s,this.localStream)})}async createOffer(e){const t=this.peers.get(e);if(!t)throw new Error(`Peer not found: ${e}`);try{const s=await t.connection.createOffer();return await t.connection.setLocalDescription(s),this.logger.debug(`Offer created for ${e}`),t.connection.localDescription}catch(t){throw this.logger.error(`Failed to create offer for ${e}:`,t),t}}async createAnswer(e){const t=this.peers.get(e);if(!t)throw new Error(`Peer not found: ${e}`);try{const s=await t.connection.createAnswer();return await t.connection.setLocalDescription(s),this.logger.debug(`Answer created for ${e}`),t.connection.localDescription}catch(t){throw this.logger.error(`Failed to create answer for ${e}:`,t),t}}async handleRemoteDescription(e,t){const s=this.peers.get(e);if(!s)throw new Error(`Peer not found: ${e}`);const{connection:i,polite:n,makingOffer:r}=s,a="offer"===t.type&&(r||"stable"!==i.signalingState);if(s.ignoreOffer=!n&&a,s.ignoreOffer)this.logger.debug(`Ignoring offer collision from ${e} (I am impolite)`);else try{if(a&&n&&(this.logger.debug(`Offer collision detected, rolling back local offer for ${e} (I am polite)`),await i.setLocalDescription({type:"rollback"})),s.isSettingRemoteAnswerPending="answer"===t.type,await i.setRemoteDescription(t),s.isSettingRemoteAnswerPending=!1,this.logger.debug(`Remote ${t.type} set for ${e}`),"offer"===t.type){const t=await this.createAnswer(e);this.emit("answerCreated",{peerId:e,answer:t})}}catch(t){throw this.logger.error(`Failed to set remote description for ${e}:`,t),t}}async addIceCandidate(e,t){const s=this.peers.get(e);if(!s)return this.logger.warn(`Peer not found for ICE candidate: ${e}`),!1;if(!s.connection.remoteDescription)return this.logger.debug(`Remote description not set yet for ${e}, ICE candidate queued`),!1;try{return await s.connection.addIceCandidate(t),this.logger.debug(`ICE candidate added for ${e}`),!0}catch(t){return s.ignoreOffer||this.logger.error(`Failed to add ICE candidate for ${e}:`,t),!1}}getPeerConnection(e){const t=this.peers.get(e);return t?t.connection:null}getPeerIds(){return Array.from(this.peers.keys())}closePeerConnection(e){const t=this.peers.get(e);t&&(t.connection.close(),this.peers.delete(e),this.emit("peerClosed",{peerId:e}),this.logger.info(`Peer connection closed: ${e}`))}closeAllPeerConnections(){this.peers.forEach((e,t)=>{e.connection.close(),this.logger.debug(`Peer connection closed: ${t}`)}),this.peers.clear(),this.emit("allPeersClosed",{})}async replaceTrack(e,t){const s=[];this.peers.forEach((i,n)=>{const r=i.connection.getSenders().find(t=>t.track&&t.track.kind===e.kind);r&&s.push(r.replaceTrack(t).then(()=>this.logger.debug(`Track replaced for ${n}`)).catch(e=>this.logger.error(`Failed to replace track for ${n}:`,e)))}),await Promise.all(s)}async getStats(e){const t=this.peers.get(e);return t?t.connection.getStats():null}getConnectionSummary(){const e={};return this.peers.forEach((t,s)=>{e[s]={connectionState:t.connection.connectionState,iceConnectionState:t.connection.iceConnectionState,signalingState:t.connection.signalingState,polite:t.polite}}),e}setLogLevel(e){this.logger.setLevel(e)}destroy(){this.closeAllPeerConnections(),this.localStream=null,this.removeAllListeners(),this.logger.info("PeerConnectionManager destroyed")}}class N extends i{constructor(e){super(),this.connectionManager=e.connectionManager,this.apiClient=e.apiClient,this.userId=e.userId,this.logLevel=e.logLevel||p.WARN,this.logger=new m(this.logLevel,"WebRTCClient"),this.mediaManager=new D({logLevel:this.logLevel}),this.peerManager=new L({iceServers:e.iceServers||g.iceServers,logLevel:this.logLevel}),this.currentRoom=null,this.isGroupCall=!1,this.participants=new Map,this._iceServersFetched=!1,this._waitingForCallAccept=!1,this._pendingCallTarget=null,this._pendingIceCandidates=new Map,this._setupEventHandlers()}async initializeIceServers(){if(!this._iceServersFetched)try{const e=await this.getTurnCredentials();e&&e.iceServers&&(this.peerManager.setIceServers(e.iceServers),this._iceServersFetched=!0,this.logger.info("ICE servers fetched successfully"))}catch(e){this.logger.warn("Failed to fetch TURN credentials, using fallback ICE servers:",e.message),this._setFallbackIceServers()}}_setFallbackIceServers(){this.peerManager.setIceServers([{urls:"stun:stun.l.google.com:19302"},{urls:"stun:stun1.l.google.com:19302"}])}_setupEventHandlers(){this.mediaManager.on("streamStarted",e=>this.emit("localStreamStarted",e)),this.mediaManager.on("streamStopped",e=>this.emit("localStreamStopped",e)),this.mediaManager.on("videoToggled",()=>this._sendMediaState()),this.mediaManager.on("audioToggled",()=>this._sendMediaState()),this.mediaManager.on("screenShareStarted",e=>this.emit("screenShareStarted",e)),this.mediaManager.on("screenShareEnded",e=>this.emit("screenShareEnded",e)),this.mediaManager.on("error",e=>this.emit("error",e)),this.peerManager.on("remoteTrack",e=>this.emit("remoteTrack",e)),this.peerManager.on("peerConnected",e=>this.emit("peerConnected",e)),this.peerManager.on("peerDisconnected",e=>this.emit("peerDisconnected",e)),this.peerManager.on("peerClosed",e=>this.emit("peerClosed",e)),this.peerManager.on("error",e=>this.emit("error",e)),this.peerManager.on("iceCandidate",({peerId:e,candidate:t})=>{this._sendSignal({type:a.ICE_CANDIDATE,receiverId:e,data:{candidate:t}})}),this.peerManager.on("negotiationNeeded",({peerId:e,description:t})=>{this._sendSignal({type:a.CALL_OFFER,receiverId:e,data:{sdp:t}})}),this.peerManager.on("answerCreated",({peerId:e,answer:t})=>{this._sendSignal({type:a.CALL_ANSWER,receiverId:e,data:{sdp:t}})})}async getTurnCredentials(){return(await this.apiClient.get("/api/v1/webrtc/turn-credentials")).data}async createCallRoom(e){return this.apiClient.post("/api/v1/webrtc/rooms",e)}async getCallRoom(e){return this.apiClient.get(`/api/v1/webrtc/rooms/${e}`)}async joinCallRoomApi(e){return this.apiClient.post(`/api/v1/webrtc/rooms/${e}/join`)}async leaveCallRoomApi(e){return this.apiClient.delete(`/api/v1/webrtc/rooms/${e}/leave`)}async enableIncomingCalls(){this._incomingCallsEnabled?this.logger.debug("Incoming calls already enabled"):(await this._subscribeToDirectSignaling(),this._incomingCallsEnabled=!0,this.logger.info("Incoming calls enabled - now listening for call invitations"))}disableIncomingCalls(){if(!this._incomingCallsEnabled)return;const e=l.getWebRTCUserDestination();this.connectionManager.unsubscribe(e),this._incomingCallsEnabled=!1,this.logger.info("Incoming calls disabled")}isIncomingCallsEnabled(){return this._incomingCallsEnabled||!1}async startCall(e){const{roomId:t,isGroup:s=!1,mediaConstraints:i={video:!0,audio:!0}}=e;try{await this.initializeIceServers();const e=await this.mediaManager.getUserMedia(i);return this.peerManager.setLocalStream(e),this.currentRoom=t,this.isGroupCall=s,await this._subscribeToSignaling(t,s),this._sendSignal({type:a.JOIN_ROOM,roomId:t}),this.logger.info(`Call started in room: ${t}`),this.emit("callStarted",{roomId:t,isGroup:s,localStream:e}),e}catch(e){throw this.logger.error("Failed to start call:",e),this.emit("error",{type:r.PEER_CONNECTION_FAILED,message:e.message,error:e}),e}}async callUser(e,t={video:!0,audio:!0}){try{await this.initializeIceServers();const s=await this.mediaManager.getUserMedia(t);this.peerManager.setLocalStream(s),await this._subscribeToDirectSignaling(),this._waitingForCallAccept=!0,this._pendingCallTarget=e,this._sendSignal({type:a.CALL_REQUEST,receiverId:e}),this.currentRoom=null,this.isGroupCall=!1,this.emit("callRequested",{targetUserId:e,localStream:s})}catch(e){throw this.logger.error("Failed to call user:",e),this._waitingForCallAccept=!1,this._pendingCallTarget=null,e}}async acceptCall(e,t={video:!0,audio:!0}){try{await this.initializeIceServers();const s=await this.mediaManager.getUserMedia(t);this.peerManager.setLocalStream(s),this.peerManager.createPeerConnection(e,!0,{skipAutoNegotiation:!0}),this._sendSignal({type:a.CALL_ACCEPT,receiverId:e}),this.emit("callAccepted",{callerId:e})}catch(e){throw this.logger.error("Failed to accept call:",e),e}}rejectCall(e){this._sendSignal({type:a.CALL_REJECT,receiverId:e}),this.emit("callRejected",{callerId:e})}cancelCall(){this._waitingForCallAccept&&this._pendingCallTarget&&(this._sendSignal({type:a.CALL_CANCEL,receiverId:this._pendingCallTarget}),this.emit("callCancelled",{targetUserId:this._pendingCallTarget}),this._waitingForCallAccept=!1,this._pendingCallTarget=null,this.mediaManager.stopAll())}endCall(){this._waitingForCallAccept&&this._pendingCallTarget&&this._sendSignal({type:a.CALL_CANCEL,receiverId:this._pendingCallTarget}),this._waitingForCallAccept=!1,this._pendingCallTarget=null,this.currentRoom&&(this._sendSignal({type:a.LEAVE_ROOM,roomId:this.currentRoom}),this._unsubscribeFromSignaling()),this.peerManager.getPeerIds().forEach(e=>{this._sendSignal({type:a.CALL_END,receiverId:e})}),this.peerManager.closeAllPeerConnections(),this.mediaManager.stopAll(),this.participants.clear(),this._pendingIceCandidates.clear();const e=this.currentRoom;this.currentRoom=null,this.isGroupCall=!1,this.emit("callEnded",{roomId:e}),this.logger.info("Call ended")}toggleVideo(){return this.mediaManager.toggleVideo()}toggleAudio(){return this.mediaManager.toggleAudio()}async startScreenShare(){const e=await this.mediaManager.getDisplayMedia(),t=e.getVideoTracks()[0],s=this.mediaManager.getTrack("video");return s&&t&&await this.peerManager.replaceTrack(s,t),t.onended=async()=>{const e=this.mediaManager.getTrack("video");e&&await this.peerManager.replaceTrack(t,e),this.emit("screenShareEnded",{})},e}stopScreenShare(){const e=this.mediaManager.getScreenStream();e&&e.getTracks().forEach(e=>e.stop())}getLocalStream(){return this.mediaManager.getLocalStream()}async getDevices(){return this.mediaManager.getDevices()}async switchDevice(e,t){const s=this.mediaManager.getTrack(t),i=await this.mediaManager.switchDevice(e,t);return s&&await this.peerManager.replaceTrack(s,i),i}setVideoEnabled(e){this.mediaManager.setVideoEnabled(e),this._sendMediaState()}setAudioEnabled(e){this.mediaManager.setAudioEnabled(e),this._sendMediaState()}startDeviceChangeDetection(){this.mediaManager.startDeviceChangeDetection(),this.mediaManager.on("deviceChange",e=>{this.emit("deviceChange",e)})}stopDeviceChangeDetection(){this.mediaManager.stopDeviceChangeDetection()}async applyVideoConstraints(e){return this.mediaManager.applyVideoConstraints(e)}getVideoSettings(){return this.mediaManager.getVideoSettings()}getAudioSettings(){return this.mediaManager.getAudioSettings()}async _subscribeToSignaling(e,t){if(t){const t=l.getWebRTCDestination(e);await this.connectionManager.subscribe(t,e=>{this._handleSignal(e)})}await this._subscribeToDirectSignaling()}async _subscribeToDirectSignaling(){const e=l.getWebRTCUserDestination();await this.connectionManager.subscribe(e,e=>{this._handleSignal(e)})}_unsubscribeFromSignaling(){if(this.currentRoom&&this.isGroupCall){const e=l.getWebRTCDestination(this.currentRoom);this.connectionManager.unsubscribe(e)}if(!this._incomingCallsEnabled){const e=l.getWebRTCUserDestination();this.connectionManager.unsubscribe(e)}}_sendSignal(e){const t={type:e.type,roomId:e.roomId||this.currentRoom,receiverId:e.receiverId,data:e.data};this.connectionManager.send(l.WEBRTC_SIGNAL,t),this.logger.debug("Signal sent:",t)}_sendMediaState(){if(!this.currentRoom&&0===this.peerManager.getPeerIds().length)return;const e={videoEnabled:this.mediaManager.isVideoEnabled(),audioEnabled:this.mediaManager.isAudioEnabled()};this._sendSignal({type:a.VIDEO_STATE_CHANGED,data:e}),this.emit("mediaStateChanged",e)}async _handleSignal(e){const{type:t,senderId:s,data:i}=e;if(s!==this.userId)switch(this.logger.debug(`Signal received: ${t} from ${s}`),t){case a.JOIN_ROOM:case a.PEER_JOINED:await this._handleUserJoined(s);break;case a.LEAVE_ROOM:case a.PEER_LEFT:this._handleUserLeft(s);break;case a.CALL_OFFER:await this._handleOffer(s,i.sdp);break;case a.CALL_ANSWER:await this._handleAnswer(s,i.sdp);break;case a.ICE_CANDIDATE:await this._handleIceCandidate(s,i.candidate);break;case a.VIDEO_STATE_CHANGED:case a.AUDIO_STATE_CHANGED:this._handleMediaState(s,i);break;case a.CALL_REQUEST:this.isInCall()||this._waitingForCallAccept?(this._sendSignal({type:a.CALL_BUSY,receiverId:s}),this.emit("incomingCallWhileBusy",{callerId:s})):this.emit("incomingCall",{callerId:s});break;case a.CALL_BUSY:this._waitingForCallAccept=!1,this._pendingCallTarget=null,this.mediaManager.stopAll(),this.emit("callBusy",{userId:s});break;case a.CALL_INVITATION:this.emit("callInvitation",{callRoomId:i?.callRoomId||e.roomId,title:i?.title,hostUserId:i?.hostUserId||s,maxParticipants:i?.maxParticipants,createdAt:i?.createdAt});break;case a.CALL_ACCEPT:await this._handleCallAccepted(s);break;case a.CALL_REJECT:this._waitingForCallAccept=!1,this._pendingCallTarget=null,this.mediaManager.stopAll(),this.emit("callRejected",{userId:s});break;case a.CALL_CANCEL:this._waitingForCallAccept=!1,this._pendingCallTarget=null,this.emit("callCancelled",{userId:s});break;case a.CALL_END:this._handleCallEnded(s);break;default:this.logger.warn(`Unknown signal type: ${t}`)}}async _handleUserJoined(e){this.participants.set(e,{joinedAt:new Date});const t=this.userId<e;this.peerManager.createPeerConnection(e,t),this.emit("userJoined",{userId:e}),this.logger.info(`User joined: ${e} (I am ${t?"polite":"impolite"})`)}_handleUserLeft(e){this.participants.delete(e),this.peerManager.closePeerConnection(e),this.emit("userLeft",{userId:e}),this.logger.info(`User left: ${e}`)}async _handleOffer(e,t){if(!this.peerManager.getPeerConnection(e)){const t=this.userId<e;this.peerManager.createPeerConnection(e,t,{skipAutoNegotiation:!0})}await this.peerManager.handleRemoteDescription(e,t),await this._processPendingIceCandidates(e)}async _handleAnswer(e,t){await this.peerManager.handleRemoteDescription(e,t),await this._processPendingIceCandidates(e)}async _handleIceCandidate(e,t){if(!this.peerManager.getPeerConnection(e))return void this._queueIceCandidate(e,t);await this.peerManager.addIceCandidate(e,new RTCIceCandidate(t))||this._queueIceCandidate(e,t)}_queueIceCandidate(e,t){this._pendingIceCandidates.has(e)||this._pendingIceCandidates.set(e,[]),this._pendingIceCandidates.get(e).push(t),this.logger.debug(`ICE candidate queued for ${e}`)}async _processPendingIceCandidates(e){const t=this._pendingIceCandidates.get(e);if(t&&0!==t.length){this.logger.debug(`Processing ${t.length} pending ICE candidates for ${e}`);for(const s of t)await this.peerManager.addIceCandidate(e,new RTCIceCandidate(s));this._pendingIceCandidates.delete(e)}}_handleMediaState(e,t){this.emit("participantMediaState",{userId:e,videoEnabled:t.videoEnabled,audioEnabled:t.audioEnabled})}async _handleCallAccepted(e){this._waitingForCallAccept=!1,this._pendingCallTarget=null,this.peerManager.createPeerConnection(e,!1,{skipAutoNegotiation:!0});const t=await this.peerManager.createOffer(e);this._sendSignal({type:a.CALL_OFFER,receiverId:e,data:{sdp:t}}),this.emit("callAccepted",{userId:e})}_handleCallEnded(e){this.peerManager.closePeerConnection(e),this.participants.delete(e),this.emit("participantLeft",{userId:e}),0===this.peerManager.getPeerIds().length&&this.endCall()}isInCall(){return null!==this.currentRoom||this.peerManager.getPeerIds().length>0}getCurrentRoom(){return this.currentRoom}getParticipants(){return Array.from(this.participants.keys())}getConnectionSummary(){return this.peerManager.getConnectionSummary()}async getStats(e){return this.peerManager.getStats(e)}getMediaState(){return{videoEnabled:this.mediaManager.isVideoEnabled(),audioEnabled:this.mediaManager.isAudioEnabled(),streamInfo:this.mediaManager.getStreamInfo()}}setLogLevel(e){this.logger.setLevel(e),this.mediaManager.setLogLevel(e),this.peerManager.setLogLevel(e)}destroy(){this.endCall(),this.mediaManager.destroy(),this.peerManager.destroy(),this.removeAllListeners(),this.logger.info("WebRTCClient destroyed")}}const P=Object.freeze({UNSUPPORTED_BROWSER:"UNSUPPORTED_BROWSER",FIREBASE_NOT_INSTALLED:"FIREBASE_NOT_INSTALLED",SW_REGISTER_FAILED:"SW_REGISTER_FAILED",PERMISSION_DENIED:"PERMISSION_DENIED",TOKEN_FAILED:"TOKEN_FAILED",SERVER_REGISTER_FAILED:"SERVER_REGISTER_FAILED"});class O extends Error{constructor(e,t,s){super(t),this.name="PushError",this.code=e,void 0!==s&&(this.cause=s)}}const U={apiKey:"AIzaSyB8VhRg6rSvUI7K3Ua7h6sBpLvaQGmIkRc",authDomain:"chatting-c3e5d.firebaseapp.com",projectId:"chatting-c3e5d",storageBucket:"chatting-c3e5d.firebasestorage.app",messagingSenderId:"1020496565673",appId:"1:1020496565673:web:5039167257fd83f5ce20b8"},$="pendingNavigation",x="pending-room",F="deviceInfo",j="device-id",W="talkflow_device_id";function V(){return"undefined"==typeof indexedDB?Promise.resolve(null):new Promise((e,t)=>{const s=indexedDB.open("talkflowPush",2);s.onupgradeneeded=()=>{const e=s.result;e.objectStoreNames.contains($)||e.createObjectStore($,{keyPath:"id"}),e.objectStoreNames.contains(F)||e.createObjectStore(F,{keyPath:"id"})},s.onsuccess=()=>e(s.result),s.onerror=()=>t(s.error||new Error("IndexedDB open failed"))})}function G(){return V().then(e=>e?new Promise((t,s)=>{const i=e.transaction(F,"readonly"),n=i.objectStore(F).get(j);n.onsuccess=()=>{const e=n.result;t(e&&e.value?e.value:null)},n.onerror=()=>s(n.error||new Error("IndexedDB read failed")),i.oncomplete=()=>e.close(),i.onerror=()=>e.close(),i.onabort=()=>e.close()}):null)}function q(e){return V().then(t=>!!t&&new Promise(s=>{try{const i=t.transaction(F,"readwrite");i.objectStore(F).put({id:j,value:e}),i.oncomplete=()=>{t.close(),s(!0)},i.onerror=()=>{t.close(),s(!1)},i.onabort=()=>{t.close(),s(!1)}}catch(e){try{t.close()}catch(e){}s(!1)}})).catch(()=>!1)}function J(){return"web-"+("undefined"!=typeof crypto&&crypto.randomUUID?crypto.randomUUID():Date.now().toString(36)+Math.random().toString(36).substring(2))}async function H(e){const t=await V();if(!t)return null;const s=function(e){return e&&"string"==typeof e&&""!==e.trim()?"pending-room:"+e:x}(e),i=!!e&&s!==x;return new Promise((n,r)=>{const a=t.transaction($,"readwrite"),o=a.objectStore($);let c=null;const l=o.get(s);l.onsuccess=()=>{const t=l.result;if(t&&t.roomId){if((t.projectId||null)===(e||null))return c=t.roomId,void o.delete(s)}if(!i)return;const n=o.get(x);n.onsuccess=()=>{const e=n.result;e&&e.roomId&&!e.projectId&&(c=e.roomId,o.delete(x))}},l.onerror=()=>{r(l.error||new Error("IndexedDB read failed"))},a.oncomplete=()=>{t.close(),n(c)},a.onerror=()=>{t.close(),r(a.error||new Error("IndexedDB transaction failed"))},a.onabort=()=>{t.close(),r(a.error||new Error("IndexedDB transaction aborted"))}})}class K{constructor(e){this.apiClient=e.apiClient,this.projectId=e.projectId||null,this.firebaseConfig=e.firebaseConfig||U,this.vapidKey=e.vapidKey||"BGP8qaSm1ntjWd4n9pc0lX_rw4BmMxm9u4pvRIANCitbmYaV0iy-gn05suTKBo88kUBejxdxM8sb6x2nt3avu8c",this.serviceWorkerPath=e.serviceWorkerPath||"/firebase-messaging-sw.js",this.logger=new m(e.logLevel||p.WARN,"PushManager"),this._messaging=null,this._currentToken=null,this._enabled=!1,this._enablePromise=null,this._foregroundMessageUnsubscribe=null,this._swRegistration=null,this._projectRegisteredToSW=!1,this._visibilityHandler=null,this._deviceIdCache=null}static async consumePendingRoom(e){return H(e)}static getPermissionState(){return"undefined"==typeof window||"undefined"==typeof Notification?"unsupported":"undefined"!=typeof navigator&&"serviceWorker"in navigator?Notification.permission:"unsupported"}async enable(){if(this._enabled)this.logger.debug("푸시 알림이 이미 활성화되어 있습니다.");else{if(this._enablePromise)return this.logger.debug("푸시 알림 활성화가 이미 진행 중입니다."),this._enablePromise;this._enablePromise=this._enableInternal();try{return await this._enablePromise}finally{this._enablePromise=null}}}async _enableInternal(){if("undefined"==typeof window||!("Notification"in window))throw new O(P.UNSUPPORTED_BROWSER,"푸시 알림은 브라우저 환경에서만 사용 가능합니다");if(!("serviceWorker"in navigator))throw new O(P.UNSUPPORTED_BROWSER,"이 브라우저는 서비스 워커를 지원하지 않습니다");if("denied"===Notification.permission)throw new O(P.PERMISSION_DENIED,"알림 권한이 이미 거부되어 있습니다. 브라우저 사이트 설정에서 허용해주세요.");let e,t,s;try{e=await import("firebase/app"),t=await import("firebase/messaging")}catch(e){throw new O(P.FIREBASE_NOT_INSTALLED,"firebase 패키지가 설치되지 않았습니다. npm install firebase 를 실행하세요.",e)}try{s=e.getApp("talkflow")}catch{s=e.initializeApp(this.firebaseConfig,"talkflow")}const i=await this._registerServiceWorker();if("granted"!==("granted"===Notification.permission?"granted":await Notification.requestPermission()))throw new O(P.PERMISSION_DENIED,"알림 권한이 거부되었습니다. 브라우저 설정에서 허용해주세요.");this._messaging=t.getMessaging(s);const n={serviceWorkerRegistration:i};this.vapidKey&&(n.vapidKey=this.vapidKey);try{this._currentToken=await t.getToken(this._messaging,n)}catch(e){throw new O(P.TOKEN_FAILED,"FCM 토큰 획득 중 오류 발생: "+(e?.message||e),e)}if(!this._currentToken)throw new O(P.TOKEN_FAILED,"FCM 토큰 획득 실패 (응답이 비어있음)");await this._registerTokenToServer(this._currentToken),this._foregroundMessageUnsubscribe&&this._foregroundMessageUnsubscribe(),this._foregroundMessageUnsubscribe=t.onMessage(this._messaging,e=>{this.logger.debug("포그라운드 푸시 수신:",e),this._onForegroundMessage(e)}),this._swRegistration=i,await this._registerProjectToSW(),this._installVisibilityReRegister();try{await i.update()}catch(e){this.logger.debug("SW 업데이트 체크 실패 (무시):",e.message)}const r=await this._getSWVersion(i);r?this.logger.debug("SW 버전:",r):this.logger.warn("[TalkFlow] 서비스 워커가 구버전(v1)입니다. 이미지 미리보기 등 리치 알림 기능을 사용하려면 firebase-messaging-sw.js 를 v2 로 업데이트하세요. 가이드: https://docs.talkflow.ai/push/sw-upgrade"),this._enabled=!0,this.logger.info("푸시 알림 활성화 완료")}async _registerServiceWorker(){try{const e=await navigator.serviceWorker.register(this.serviceWorkerPath,{updateViaCache:"none"});return this.logger.debug("서비스 워커 등록 완료:",this.serviceWorkerPath),e}catch(e){throw new O(P.SW_REGISTER_FAILED,`서비스 워커 등록 실패: ${this.serviceWorkerPath}\n프로젝트 public 폴더에 firebase-messaging-sw.js 파일을 배치하세요.`,e)}}async _registerTokenToServer(e){try{const t=await this._getDeviceId();this.apiClient?.authProvider?.setDeviceId(t),await this.apiClient.post("/api/v1/push/devices",{deviceToken:e,deviceType:"WEB",deviceId:t}),this.logger.info("디바이스 토큰 서버 등록 완료")}catch(e){if(this.logger.error("디바이스 토큰 서버 등록 실패:",e),e instanceof O)throw e;throw new O(P.SERVER_REGISTER_FAILED,"디바이스 토큰 서버 등록 실패: "+(e?.message||e),e)}}async _getDeviceId(){if(this._deviceIdCache)return this._deviceIdCache;try{let e=await G();if(!e&&"undefined"!=typeof localStorage){const t=localStorage.getItem(W);if(t){e=t;if(await q(e)){if(await G()===e)try{localStorage.removeItem(W),this.logger.debug("deviceId migrated from localStorage to IndexedDB")}catch(e){}else this.logger.warn("deviceId IDB read-back 불일치, localStorage 보존")}else this.logger.warn("deviceId IDB 저장 실패 (private mode / quota 등), localStorage 보존")}}if(!e){e=J();await q(e)||this.logger.warn("신규 deviceId IDB 저장 실패, 이번 세션만 유효")}return this._deviceIdCache=e,this.apiClient?.authProvider?.setDeviceId(e),e}catch(e){if(this.logger.warn("IndexedDB deviceId 조회/저장 실패, fallback:",e),!this._deviceIdCache){let e=null;if("undefined"!=typeof localStorage)try{e=localStorage.getItem(W)}catch(e){}this._deviceIdCache=e||J()}return this._deviceIdCache}}_onForegroundMessage(e){}async consumePendingRoom(){return K.consumePendingRoom(this.projectId)}reset(){if(this._foregroundMessageUnsubscribe)try{this._foregroundMessageUnsubscribe()}catch(e){this.logger.debug("포그라운드 푸시 리스너 해제 실패 (무시):",e?.message||e)}this._uninstallVisibilityReRegister(),this._unregisterProjectFromSW(),this._foregroundMessageUnsubscribe=null,this._enablePromise=null,this._messaging=null,this._currentToken=null,this._enabled=!1,this._swRegistration=null,this._projectRegisteredToSW=!1}async _registerProjectToSW(){if(this.projectId)try{const e=(await navigator.serviceWorker.ready).active||this._swRegistration&&this._swRegistration.active;if(!e)return void this.logger.debug("SW active worker 없음 — projectId 등록 스킵");e.postMessage({type:"TALKFLOW_REGISTER_PROJECT",projectId:this.projectId}),this._projectRegisteredToSW=!0}catch(e){this.logger.debug("SW projectId 등록 실패 (무시):",e?.message||e)}}_unregisterProjectFromSW(){if(this._projectRegisteredToSW)try{if("undefined"==typeof navigator||!navigator.serviceWorker)return;const e=navigator.serviceWorker.controller;if(!e)return;e.postMessage({type:"TALKFLOW_UNREGISTER_PROJECT"})}catch(e){this.logger.debug("SW projectId 해제 실패 (무시):",e?.message||e)}}_installVisibilityReRegister(){"undefined"==typeof document||this._visibilityHandler||(this._visibilityHandler=()=>{"visible"===document.visibilityState&&this._enabled&&this.projectId&&this._registerProjectToSW().catch(()=>{})},document.addEventListener("visibilitychange",this._visibilityHandler))}_uninstallVisibilityReRegister(){if("undefined"!=typeof document&&this._visibilityHandler){try{document.removeEventListener("visibilitychange",this._visibilityHandler)}catch(e){}this._visibilityHandler=null}}getToken(){return this._currentToken}isEnabled(){return this._enabled}async getMyDevices(){const e=await this.apiClient.get("/api/v1/push/devices/me");return e&&"object"==typeof e&&"data"in e?e.data:e}async setDeviceEnabled(e,t){await this.apiClient.patch("/api/v1/push/devices/me/enabled",{deviceId:e,enabled:t}),this.logger.info(`디바이스 푸시 enabled=${t}: deviceId=${e}`)}async setCurrentDeviceEnabled(e){const t=await this._getDeviceId();await this.setDeviceEnabled(t,e)}async _getSWVersion(e){try{const t=await navigator.serviceWorker.ready,s=t.waiting||t.installing||t.active||e.waiting||e.installing||e.active;return s?new Promise(e=>{const t=setTimeout(()=>e(null),2e3),i=new MessageChannel;i.port1.onmessage=s=>{clearTimeout(t),e(s.data?.version||null)};try{s.postMessage({type:"TALKFLOW_SW_VERSION"},[i.port2])}catch(s){clearTimeout(t),e(null)}}):null}catch{return null}}setLogLevel(e){this.logger.setLevel(e)}}const z=[["connected","connected"],["disconnected","disconnected"],["reconnecting","reconnecting"],["reconnected","reconnected"],["resumed","resumed"],["error","connectionError"]],B=[["syncStarted","syncStarted"],["syncCompleted","syncCompleted"],["gapTruncated","gapTruncated"],["roomListSynced","roomListSynced"],["message","chatMessage"],["newMessage","newChatMessage"],["messageUpdated","messageUpdated"],["messageDeleted","messageDeleted"],["reactionChanged","reactionChanged"],["linkPreviewAttached","linkPreviewAttached"],["messageTranslated","messageTranslated"],["messageRead","messageRead"],["typing","typing"],["assistantProgress","assistantProgress"],["assistantRoomProgress","assistantRoomProgress"],["summaryUpdate","summaryUpdate"],["memberJoined","memberJoined"],["memberLeft","memberLeft"],["roomSubscribed","roomSubscribed"],["roomUnsubscribed","roomUnsubscribed"],["roomListSubscribed","roomListSubscribed"],["roomListUnsubscribed","roomListUnsubscribed"],["roomListUpdate","roomListUpdate"],["roomListMessage","roomListMessage"],["roomListMessageDeleted","roomListMessageDeleted"],["roomListMessageUpdated","roomListMessageUpdated"],["roomListCreated","roomListCreated"],["roomListJoined","roomListJoined"],["roomListLeft","roomListLeft"],["roomListSelfLeft","roomListSelfLeft"],["roomListKicked","roomListKicked"],["roomListSelfKicked","roomListSelfKicked"],["roomListBanned","roomListBanned"],["roomListSelfBanned","roomListSelfBanned"],["roomListOwnerChanged","roomListOwnerChanged"],["roomListDeleted","roomListDeleted"],["roomListRoomUpdated","roomListRoomUpdated"],["roomListUnreadUpdated","roomListUnreadUpdated"],["retentionCleanup","retentionCleanup"]],X=[["localStreamStarted","localStreamStarted"],["localStreamStopped","localStreamStopped"],["remoteTrack","remoteTrack"],["screenShareStarted","screenShareStarted"],["screenShareEnded","screenShareEnded"],["deviceChange","deviceChange"],["mediaStateChanged","mediaStateChanged"],["callStarted","callStarted"],["callEnded","callEnded"],["callRequested","callRequested"],["callAccepted","callAccepted"],["callRejected","callRejected"],["callCancelled","callCancelled"],["callBusy","callBusy"],["incomingCall","incomingCall"],["incomingCallWhileBusy","incomingCallWhileBusy"],["callInvitation","callInvitation"],["userJoined","userJoined"],["userLeft","userLeft"],["participantLeft","participantLeft"],["participantMediaState","participantMediaState"],["peerConnected","peerConnected"],["peerDisconnected","peerDisconnected"],["peerClosed","peerClosed"],["error","webrtcError"]];function Y(e,t,s){s.forEach(([s,i])=>{e.on(s,e=>{t.emit(i,e)})})}const Z=["getRooms","getRoom","getRoomInfo","setMyRoomLanguage","createOneToOneRoom","createGroupRoom","joinGroupRoom","leaveRoom","transferOwnership","deleteRoom","updateGroupRoom","inviteToGroupRoom","kickMember","banMember","unbanMember","getBannedMembers","getAvailableGroupRooms","getAllGroupRooms","enterRoom","getMessages","fetchLinkPreview","sendMessage","sendMessageOptimistic","sendTextMessage","sendTextMessageOptimistic","sendReply","sendReplyOptimistic","sendDecisionChoice","uploadFile","sendFileMessage","sendFileMessageOptimistic","classifyFileType","resolveMessageCategory","editMessage","deleteMessage","markAsRead","pinMessage","unpinMessage","toggleReaction","subscribeRoom","unsubscribeRoom","unsubscribeAllRooms","setActiveRoom","clearActiveRoom","getActiveRoom","subscribeRoomList","unsubscribeRoomList","isRoomListSubscribed","getSubscribedRooms","isSubscribed","startTyping","stopTyping","getAssistantProgress","resumeAssistantStreams","getAssistants","getRoomAiMeta","rateAssistantMessage","summarizeWithAssistant","translateWithAssistant","uploadVideoAsset","analyzeVideo","analyzeVideoWithAssistant","getVideoAnalysis","cancelVideoAnalysis","getVideoAnalysisCapabilities","refreshSummary","getSummary","getRoomPmPrompt","upsertRoomPmPrompt","activateRoomPmPrompt","deactivateRoomPmPrompt","getRoomPmPromptVersions","activateRoomPmPromptVersion","previewRoomPmPrompt","createVote","getVotes","getVote","updateVote","deleteVote","closeVote","submitVote","uploadVoteImage"],Q=["initializeIceServers","getTurnCredentials","createCallRoom","getCallRoom","joinCallRoomApi","leaveCallRoomApi","enableIncomingCalls","disableIncomingCalls","isIncomingCallsEnabled","startCall","callUser","acceptCall","rejectCall","cancelCall","endCall","toggleVideo","toggleAudio","setVideoEnabled","setAudioEnabled","startScreenShare","stopScreenShare","getLocalStream","getDevices","switchDevice","startDeviceChangeDetection","stopDeviceChangeDetection","applyVideoConstraints","getVideoSettings","getAudioSettings","isInCall","getCurrentRoom","getParticipants","getMediaState","getConnectionSummary","getStats"];function ee(e,t,s){s.forEach(s=>{e[s]=function(...e){return this[t][s](...e)}})}class te extends i{constructor(e){super(),this._validateOptions(e);const t=e.env||g.environment,s=(e.serverUrl||u(t)).replace(/\/$/,"");this.options={serverUrl:s,env:t,apiKey:e.apiKey,projectId:e.projectId,jwtToken:e.jwtToken||null,sessionMode:e.sessionMode||(f(e.jwtToken||"").sid?"refresh":"legacy"),useSockJS:!1!==e.useSockJS,reconnectDelay:e.reconnectDelay||g.reconnectDelay,maxReconnectDelay:e.maxReconnectDelay||g.maxReconnectDelay,maxReconnectAttempts:e.maxReconnectAttempts||g.maxReconnectAttempts,heartbeatIncoming:e.heartbeatIncoming||g.heartbeatIncoming,heartbeatOutgoing:e.heartbeatOutgoing||g.heartbeatOutgoing,resumeOnVisibility:!1!==e.resumeOnVisibility,syncOnReconnect:!1!==e.syncOnReconnect,assistantStreamReplay:!1!==e.assistantStreamReplay,rewindMillis:void 0!==e.rewindMillis?e.rewindMillis:g.rewindMillis,maxSyncPages:e.maxSyncPages||g.maxSyncPages,syncPageSize:e.syncPageSize||g.syncPageSize,iceServers:e.iceServers||g.iceServers,autoSubscribeRoomList:!1!==e.autoSubscribeRoomList,logLevel:void 0!==e.logLevel?e.logLevel:p.WARN},this.logger=new m(this.options.logLevel,"TalkFlowClient"),this._initialized=!1,this._state=n.DISCONNECTED,this._userId=null,this.options.jwtToken&&(this._userId=v(this.options.jwtToken)),this.apiClient=new _({baseUrl:this.options.serverUrl,apiKey:this.options.apiKey,projectId:this.options.projectId,jwtToken:this.options.jwtToken,logLevel:this.options.logLevel}),this.authSession=null,this._authConnectionRequested=!1,this.connectionManager=null,this._chat=null,this._webrtc=null,this._pushManager=null,this._pushEnablePromise=null,this.options.jwtToken&&this._userId&&this._initializeSubClients(),this._initialized=!0,"refresh"===this.options.sessionMode&&this._enableRefreshSession(),this.logger.info("TalkFlowClient initialized",{userId:this._userId,hasToken:!!this.options.jwtToken})}get chat(){return this._chat}get webrtc(){return this._webrtc}get pushManager(){return this._pushManager}get userId(){return this._userId}_validateOptions(e){if(!e)throw new Error("Options are required");if(!e.apiKey&&"refresh"!==e.sessionMode)throw new Error("apiKey is required");if(!e.projectId&&"refresh"!==e.sessionMode)throw new Error("projectId is required");e.apiKey?.startsWith("sk-")&&console.error("🚨 [TalkFlow] Server Key (sk-) 로 SDK 를 초기화했습니다. SDK 는 Client Key (ck-) 만 사용합니다 — loginUser() 응답의 clientKey 를 그대로 쓰세요.")}_initializeSubClients(){!function(e){if(!e.connectionManager){if(!e.userId)throw new Error("userId is required to initialize sub-clients. Please set JWT token first.");e.connectionManager=new C({serverUrl:e.options.serverUrl,jwtToken:e.options.jwtToken,apiKey:e.options.apiKey,projectId:e.options.projectId,useSockJS:e.options.useSockJS,reconnectDelay:e.options.reconnectDelay,maxReconnectDelay:e.options.maxReconnectDelay,maxReconnectAttempts:e.options.maxReconnectAttempts,heartbeatIncoming:e.options.heartbeatIncoming,heartbeatOutgoing:e.options.heartbeatOutgoing,resumeOnVisibility:e.options.resumeOnVisibility,logLevel:e.options.logLevel}),e.connectionManager.getAccessToken=async({forceRefresh:t=!1}={})=>e.authSession?e.authSession.getAccessToken({forceRefresh:t,rejectedToken:e.connectionManager?.jwtToken}):e.options.jwtToken,e._chat=new k({connectionManager:e.connectionManager,apiClient:e.apiClient,userId:e._userId,syncOnReconnect:e.options.syncOnReconnect,assistantStreamReplay:e.options.assistantStreamReplay,rewindMillis:e.options.rewindMillis,maxSyncPages:e.options.maxSyncPages,syncPageSize:e.options.syncPageSize,logLevel:e.options.logLevel}),e._webrtc=new N({connectionManager:e.connectionManager,apiClient:e.apiClient,userId:e._userId,iceServers:e.options.iceServers,logLevel:e.options.logLevel}),e._setupEventForwarding(),e.logger.debug("Sub-clients initialized")}}(this)}_setupEventForwarding(){var e;(e=this).connectionManager&&(e.connectionManager.on("stateChange",({state:t,prevState:s})=>{e._state=t,e.emit("stateChange",{state:t,prevState:s})}),Y(e.connectionManager,e,z),e.chat&&Y(e.chat,e,B),e.webrtc&&Y(e.webrtc,e,X))}isConnected(){return!!this.connectionManager&&this.connectionManager.isConnected()}_enableRefreshSession(){this.authSession||(this.options.sessionMode="refresh",this.authSession=w.forServer(this.options.serverUrl),this.authSession.attach(this),this.apiClient.authProvider=this.authSession,this._authUnsubscribe=this.authSession.on("authStateChanged",e=>this.emit("authStateChanged",e)),this._authErrorUnsubscribe=this.authSession.on("authError",e=>this.emit("authError",e)),!this.authSession.data&&this.options.jwtToken&&f(this.options.jwtToken).sid&&(this.authSession.data={accessToken:this.options.jwtToken,projectId:this.options.projectId,clientKey:this.options.apiKey,user:{userId:this.userId}},this.authSession._setState("authenticated")),this._authReady=Promise.resolve(),this.authSession.data&&(this._authReady=this._acceptAuthSession(this.authSession.data).catch(e=>{throw this._authReady=Promise.resolve(),e})))}async _acceptAuthSession(e){this.options.projectId&&this.options.projectId!==e.projectId&&this.connectionManager&&await this._clearAuthSession(),this.options.apiKey=e.clientKey,this.options.projectId=e.projectId,this.apiClient.apiKey=e.clientKey,this.apiClient.projectId=e.projectId,await this.setToken(e.accessToken)}async _clearAuthSession(){this._authConnectionRequested=!1,this.options.jwtToken=null,this._userId=null,this.apiClient.setJwtToken(null),await this.disconnect(),this._chat?.destroy(),this._webrtc?.destroy(),await(this.connectionManager?.destroy()),this._chat=null,this._webrtc=null,this.connectionManager=null,this.pushManager?.reset(),this._pushManager=null,this._pushEnablePromise=null,this.emit("loggedOut")}getAuthState(){return this.authSession?.getAuthState()||(1e3*Number(f(this.options.jwtToken||"").exp||0)>Date.now()?"authenticated":"unauthenticated")}isLoggedIn(){return"authenticated"===this.getAuthState()}async getAccessToken(e={}){if(this.authSession)return await this._authReady,this.authSession.getAccessToken(e);if(!this.isLoggedIn()){const e=new Error("Sign in is required");throw e.code="SESSION_REVOKED",e}return this.options.jwtToken}async restoreSession(){return this._enableRefreshSession(),await this._authReady,this.authSession.restoreSession()}static async restoreSession({serverUrl:e}){if(!e)throw new Error("serverUrl is required");return w.forServer(e).restoreSession()}async request(e,t,s={}){if(!t.startsWith("/")||t.startsWith("//"))throw new Error("Use an API-relative path");return await this._authReady,this.apiClient.request(e,t,s)}getState(){return this._state}hasToken(){return!!this.options.jwtToken}static async loginUser({serverUrl:e,email:t,password:s}){if(!e)throw new Error("serverUrl is required");if(!t)throw new Error("email is required");if(!s)throw new Error("password is required");return w.forServer(e).login({email:t,password:s})}async loginUser({email:e,password:t}){if(!e||!t)throw new Error("email and password are required");return this._enableRefreshSession(),await this._authReady,this.authSession.login({email:e,password:t})}async getMyProfile(){this._checkToken();const e=await this.apiClient.get("/api/v1/me");return e&&e.data?e.data:e}async updateMyProfile(e){this._checkToken();const t=await this.apiClient.put("/api/v1/me",e),s=t&&t.data?t.data:t;if(s&&s.accessToken)try{await this.setToken(s.accessToken)}catch(e){this.logger.warn("Profile updated but token refresh failed; keeping the old token",e)}return s}async changeMyPassword({currentPassword:e,newPassword:t}){if(this._checkToken(),this.authSession)return this.authSession.changePassword(()=>this.apiClient.request("PUT","/api/v1/me/password",{body:{currentPassword:e,newPassword:t},skipAuth:!0}));const s=await this.apiClient.put("/api/v1/me/password",{currentPassword:e,newPassword:t}),i=s?.data??s;return i?.accessToken&&i?.refreshToken&&(this._enableRefreshSession(),this.authSession._newMarker(!1),await this.authSession._accept(i)),s}async setPreferredLanguage(e){return this.updateMyProfile({preferredLanguage:e})}static detectBrowserLanguage(){return"undefined"!=typeof navigator&&navigator.language?te.normalizeLanguageCode(navigator.language):null}static normalizeLanguageCode(e){if(!e||"string"!=typeof e)return null;const t=e.trim();if(!t)return null;if("OFF"===t.toUpperCase())return"OFF";const s=t.toLowerCase(),i=s.indexOf("-");if(i<0)return s;const n=s.slice(0,i);return"zh"===n?s:n}static displayText(e,t){const s=te.normalizeLanguageCode(t);return e&&e.translations&&s&&e.translations[s]?e.translations[s]:e?e.content:null}async getCompanyDirectory(){return this._checkToken(),this.apiClient.get("/api/v1/company/directory")}async getUsers(e={}){this._checkToken();const{size:t=50,lastId:s,lastSortValue:i}=e;return this.apiClient.get("/api/v1/users",{size:t,lastId:s,lastSortValue:i})}async searchUsers(e){this._checkToken();const{keyword:t,limit:s=20}=e;return this.apiClient.get("/api/v1/users/search",{keyword:t,limit:s})}_checkToken(){if(!this.options.jwtToken)throw new Error('JWT token is required for this operation. Sign in with TalkFlowClient.loginUser({ serverUrl, email, password }) and pass the returned accessToken via setToken(). See README "로그인".')}getUserId(){return this.userId}isInitialized(){return this._initialized}isReady(){return this._initialized&&!!this.connectionManager}setLogLevel(e){this.options.logLevel=e,this.logger.setLevel(e),this.apiClient.logger?.setLevel(e),this.connectionManager&&this.connectionManager.setLogLevel(e),this.chat&&this.chat.setLogLevel(e),this.webrtc&&this.webrtc.setLogLevel(e)}getStatus(){return{initialized:this._initialized,ready:this.isReady(),hasToken:this.hasToken(),connectionState:this._state,isConnected:this.isConnected(),userId:this.userId,chat:this.chat?{subscribedRooms:this.chat.getSubscribedRooms()}:null,webrtc:this.webrtc?{isInCall:this.webrtc.isInCall(),currentRoom:this.webrtc.getCurrentRoom(),participants:this.webrtc.getParticipants(),mediaState:this.webrtc.getMediaState()}:null}}async destroy(){await this.disconnect(),this._authUnsubscribe?.(),this._authErrorUnsubscribe?.(),this.authSession?.detach(this),this.chat&&this.chat.destroy(),this.webrtc&&this.webrtc.destroy(),this.connectionManager&&await this.connectionManager.destroy(),this.removeAllListeners(),this._initialized=!1,this.logger.info("TalkFlowClient destroyed")}}te.ConnectionState=n,te.ErrorTypes=r,te.LogLevel=p,te.Environment=h,function(e){e.prototype.connect=async function(e,t={}){const{enablePush:s=!1}=t;if(e&&await this.setToken(e),this.authSession){await this._authReady,this._authConnectionRequested=!0;const e=await this.authSession.getAccessToken();await this.setToken(e),this.authSession.schedule()}if(!this.options.jwtToken)throw new Error('JWT token is required. Sign in with TalkFlowClient.loginUser({ serverUrl, email, password }) and pass the returned accessToken to the SDK via the jwtToken option, setToken(), or connect(jwt). See README "로그인".');if(!this.authSession&&R(this.options.jwtToken))throw new Error("JWT token has expired. Please update the token before connecting.");if(this.connectionManager||this._initializeSubClients(),await this.connectionManager.connect(),this.webrtc&&await this.webrtc.enableIncomingCalls(),this.chat&&this.options.autoSubscribeRoomList)try{await this.chat.subscribeRoomList()}catch(e){this.logger.warn("Failed to auto-subscribe room list (non-fatal):",e)}s&&this.enablePushNotifications(),this.logger.info("Connected to server")},e.prototype.disconnect=async function(){this._authConnectionRequested=!1,this.authSession?.schedule(),this.connectionManager&&(this.webrtc&&this.webrtc.isInCall()&&this.webrtc.endCall(),this.webrtc&&this.webrtc.disableIncomingCalls(),this.chat&&(this.chat.unsubscribeAllRooms(),this.chat.unsubscribeRoomList()),await this.connectionManager.disconnect(),this.logger.info("Disconnected from server"))},e.prototype.logout=async function(){if(this.authSession)return this.authSession.logout();try{await this.apiClient.post("/v1/auth/signout"),this.logger.info("Logged out from server")}catch(e){this.logger.warn("Server logout failed (proceeding with local cleanup):",e.message)}await this.disconnect(),this.pushManager&&(this.pushManager.reset(),this._pushEnablePromise=null),this.options.jwtToken=null,this._userId=null,this.apiClient.setJwtToken(null),this.emit("loggedOut")},e.prototype.enablePushNotifications=async function(e={}){if(!this.options.jwtToken){const e=new Error("JWT token is required to enable push notifications.");return this.emit("pushFailed",{reason:"NO_TOKEN",error:e}),{ok:!1,reason:"NO_TOKEN",error:e}}if(!this.pushManager){this._pushManager=new K({apiClient:this.apiClient,projectId:this.options.projectId,firebaseConfig:e.firebaseConfig,vapidKey:e.vapidKey,serviceWorkerPath:e.serviceWorkerPath,logLevel:this.options.logLevel});const t=3e5;this.pushManager._onForegroundMessage=e=>{const s=e.data||{};if(s.projectId&&s.projectId!==this.options.projectId)return void this.logger.debug("다른 프로젝트 푸시 무시:",s.projectId);const i=this.chat?.getActiveRoom();if(i&&i===s.roomId)this.logger.debug("포그라운드 푸시 suppress (현재 방):",s.roomId);else{if(s.messageId){if(this._recentPushMessageIds||(this._recentPushMessageIds=new Map),this._recentPushMessageIds.has(s.messageId))return void this.logger.debug("포그라운드 푸시 dedup:",s.messageId);const e=setTimeout(()=>{this._recentPushMessageIds.delete(s.messageId)},t);this._recentPushMessageIds.set(s.messageId,e)}this.emit("pushNotification",{title:e.notification?.title||s.title,body:e.notification?.body||s.body,data:s})}}}if(this.pushManager.isEnabled())return this.logger.debug("웹 푸시가 이미 활성화되어 있어 중복 호출을 건너뜁니다."),{ok:!0,alreadyEnabled:!0};if(this._pushEnablePromise)return this.logger.debug("웹 푸시 활성화가 이미 진행 중입니다."),this._pushEnablePromise;const t=(async()=>{try{return await this.pushManager.enable(),this.emit("pushEnabled"),{ok:!0}}catch(e){const t=e&&e.code?e.code:"UNKNOWN";return this.logger.warn("Failed to enable push notifications:",t,e?.message),this.emit("pushFailed",{reason:t,error:e}),{ok:!1,reason:t,error:e}}})();return this._pushEnablePromise=t,t.finally(()=>{this._pushEnablePromise===t&&(this._pushEnablePromise=null)}),t},e.prototype.consumePendingRoom=async function(){return K.consumePendingRoom(this.options.projectId)},e.prototype.setCurrentDeviceEnabled=function(e){return this.pushManager?.setCurrentDeviceEnabled(e)},e.prototype.setDeviceEnabled=function(e,t){return this.pushManager?.setDeviceEnabled(e,t)},e.prototype.getMyDevices=function(){return this.pushManager?.getMyDevices()},e.prototype.getPushPermissionState=function(){return K.getPermissionState()},e.prototype.isPushEnabled=function(){return this.pushManager?.isEnabled()??!1},e.prototype.getPushToken=function(){return this.pushManager?.getToken()??null},e.prototype.resetPush=function(){this.pushManager?.reset(),this._pushEnablePromise=null},e.prototype.setToken=async function(e){const{userId:t}=T(e,{validateExpiry:!1}),s=this.userId&&this.userId!==t;this.options.jwtToken=e,this._userId=t,this.apiClient.setJwtToken(e),s&&this.connectionManager?(await this.disconnect(),this._chat?.destroy(),this._webrtc?.destroy(),await this.connectionManager.destroy(),this.pushManager&&(this.pushManager.reset(),this._pushEnablePromise=null,this._pushManager=null),this.connectionManager=null,this._chat=null,this._webrtc=null,this._initializeSubClients()):this.connectionManager?this.connectionManager.updateToken(e):this._initializeSubClients(),this.logger.info("JWT token set",{userId:t}),this.authSession?.replaceAccessToken(e),this.emit("tokenSet",{userId:t})},e.prototype.updateToken=async function(e){await this.setToken(e)}}(te),function(e){ee(e.prototype,"chat",Z),ee(e.prototype,"webrtc",Q)}(te);e.AssistantMode={GENERAL:"GENERAL",PEOPLE_ONLY:"PEOPLE_ONLY",CALL_ONLY:"CALL_ONLY"},e.ChatClient=k,e.ChatMessageType={TEXT:"TEXT",IMAGE:"IMAGE",FILE:"FILE",VIDEO:"VIDEO",AUDIO:"AUDIO",DOCUMENT:"DOCUMENT",MIXED:"MIXED",GALLERY:"GALLERY",SYSTEM:"SYSTEM"},e.ChatRoomType=o,e.ConnectionManager=C,e.ConnectionState=n,e.DecisionCardLimits={MIN_OPTIONS:2,MAX_OPTIONS:5,LABEL_MAX_LENGTH:80,QUESTION_MAX_LENGTH:200},e.DefaultConfig=g,e.Endpoints=d,e.EngagementIntensity={QUIET:"QUIET",NORMAL:"NORMAL",ACTIVE:"ACTIVE"},e.Environment=h,e.ErrorTypes=r,e.EventEmitter=i,e.LogLevel=p,e.Logger=m,e.MediaStreamManager=D,e.PeerConnectionManager=L,e.PersonaRole={LEGAL_ADVISOR:"LEGAL_ADVISOR",MARKETING:"MARKETING",PRODUCT_PLANNING:"PRODUCT_PLANNING",HR:"HR",FINANCE:"FINANCE",CUSTOMER_SUPPORT:"CUSTOMER_SUPPORT",SALES:"SALES",ENGINEERING:"ENGINEERING",DATA_ANALYST:"DATA_ANALYST",PROJECT_MANAGEMENT:"PROJECT_MANAGEMENT",RESEARCH:"RESEARCH",TRANSLATION:"TRANSLATION",DESIGN:"DESIGN",PM:"PM"},e.PmPromptLayerEditorType={SUPER_ADMIN:"SUPER_ADMIN",PROJECT_ADMIN:"PROJECT_ADMIN",ROOM_OWNER:"ROOM_OWNER"},e.PmPromptLayerScope={PROJECT:"PROJECT",ROOM:"ROOM"},e.PushError=O,e.PushErrorCode=P,e.PushManager=K,e.RoomAiType={NONE:"NONE",PERSONA_MULTI:"PERSONA_MULTI",PM_BACKSTAGE:"PM_BACKSTAGE"},e.RoomListEventType=c,e.RoomUnreadReconciler=class{constructor(e){this._currentUserId=e,this._rooms=new Map}_room(e){let t=this._rooms.get(e);return t||(t={snapshotCount:null,snapshotVersion:null,pendingDeltas:new Map},this._rooms.set(e,t)),t}applyMessageReceived(e){if(!e||!e.roomId||!e.messageId)return;if(null!=e.actorId&&e.actorId===this._currentUserId)return;const t=this._room(e.roomId);if(t.pendingDeltas.has(e.messageId))return;const s=null==e.unreadVersion?null:e.unreadVersion;null!=t.snapshotVersion&&null!=s&&s<=t.snapshotVersion||t.pendingDeltas.set(e.messageId,{version:s,delta:1})}applySnapshot(e,t,s){if(!e||null==t)return;const i=null==s?null:s,n=this._room(e);if(!(null!=n.snapshotVersion&&null==i||null!=n.snapshotVersion&&null!=i&&i<n.snapshotVersion||(n.snapshotCount=t,null!=i&&(n.snapshotVersion=i),null==i)))for(const[e,t]of n.pendingDeltas)(null==t.version||t.version<=i)&&n.pendingDeltas.delete(e)}getCount(e){const t=this._rooms.get(e);if(!t||null==t.snapshotCount)return null;let s=t.snapshotCount;for(const e of t.pendingDeltas.values())s+=e.delta;return s}removeRoom(e){this._rooms.delete(e)}clear(){this._rooms.clear()}},e.SenderType={USER:"USER",ASSISTANT:"ASSISTANT"},e.SignalTypes=a,e.SummarizeFormat={MINUTES:"MINUTES",SHORT:"SHORT",TIMELINE:"TIMELINE",ACTIONS:"ACTIONS",OPTIONS:"OPTIONS"},e.WebRTCClient=N,e.WebSocketPaths=l,e.decodeJWTPayload=function(e){try{const t=I(e).split(".");return 3!==t.length?null:JSON.parse(b(t[1]))}catch(e){return console.error("Failed to decode JWT payload:",e),null}},e.default=te,e.extractUserIdFromJWT=v,e.getJWTRemainingTime=function(e){try{const t=I(e).split(".");if(3!==t.length)return-1;const s=JSON.parse(b(t[1]));return s.exp?1e3*s.exp-Date.now():1/0}catch(e){return console.error("Failed to get JWT remaining time:",e),-1}},e.getServerUrl=u,e.isJWTExpired=R,e.validateAndParseJWT=T,Object.defineProperty(e,"__esModule",{value:!0})});
|
|
2
2
|
//# sourceMappingURL=talkflow-sdk.umd.js.map
|