@gamention/pulse-core 0.3.3 → 0.3.5

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/dist/index.d.ts CHANGED
@@ -21,12 +21,17 @@ export declare class Connection extends Emitter {
21
21
  private reconnectTimer;
22
22
  private _state;
23
23
  private permanentlyClosed;
24
+ private handleOpen;
25
+ private handleMessage;
26
+ private handleClose;
27
+ private handleError;
24
28
  get state(): ConnectionState;
25
29
  constructor(endpoint?: string);
26
30
  connect(): void;
27
31
  disconnect(): void;
28
32
  send(message: ClientMessage): void;
29
33
  permanentDisconnect(): void;
34
+ private cleanupWs;
30
35
  private scheduleReconnect;
31
36
  }
32
37
 
@@ -111,6 +116,7 @@ export declare class PulseClient extends Emitter {
111
116
  private _p2pInstance;
112
117
  private p2pEnabled;
113
118
  private iceServers;
119
+ private signalingQueue;
114
120
  /** Current WebSocket connection state. */
115
121
  get connectionState(): ConnectionState;
116
122
  constructor(config: PulseConfig);
@@ -158,6 +164,7 @@ export declare class PulseClient extends Emitter {
158
164
  /** Lazy-loaded P2P manager. Returns a promise that resolves to the P2PManager instance. */
159
165
  get p2p(): Promise<P2PManager>;
160
166
  private initP2P;
167
+ private routeP2PMessage;
161
168
  }
162
169
 
163
170
  export declare interface SharedCounter {
@@ -1 +1 @@
1
- "use strict";var f=Object.defineProperty;var _=(h,a,e)=>a in h?f(h,a,{enumerable:!0,configurable:!0,writable:!0,value:e}):h[a]=e;var r=(h,a,e)=>_(h,typeof a!="symbol"?a+"":a,e);Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const c=require("@gamention/pulse-shared");class d{constructor(){r(this,"handlers",new Map)}on(a,e){this.handlers.has(a)||this.handlers.set(a,new Set);const t=this.handlers.get(a);return t.add(e),()=>t.delete(e)}off(a,e){var t;(t=this.handlers.get(a))==null||t.delete(e)}emit(a,e){var t;(t=this.handlers.get(a))==null||t.forEach(i=>i(e))}removeAll(){this.handlers.clear()}}class p extends d{constructor(e){super();r(this,"ws",null);r(this,"endpoint");r(this,"reconnectAttempt",0);r(this,"reconnectTimer",null);r(this,"_state","disconnected");r(this,"permanentlyClosed",!1);this.endpoint=e??c.DEFAULT_ENDPOINT}get state(){return this._state}connect(){this.ws||this.permanentlyClosed||(this._state="connecting",this.emit("state",this._state),this.ws=new WebSocket(this.endpoint),this.ws.addEventListener("open",()=>{this._state="connected",this.reconnectAttempt=0,this.emit("state",this._state)}),this.ws.addEventListener("message",e=>{try{const t=JSON.parse(e.data);this.emit("message",t)}catch{}}),this.ws.addEventListener("close",()=>{this.ws=null,this._state="disconnected",this.emit("state",this._state),this.scheduleReconnect()}),this.ws.addEventListener("error",()=>{var e;(e=this.ws)==null||e.close()}))}disconnect(){var e;this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.reconnectAttempt=0,(e=this.ws)==null||e.close(),this.ws=null,this._state="disconnected",this.emit("state",this._state)}send(e){var t;((t=this.ws)==null?void 0:t.readyState)===WebSocket.OPEN&&this.ws.send(JSON.stringify(e))}permanentDisconnect(){this.permanentlyClosed=!0,this.disconnect()}scheduleReconnect(){if(this.permanentlyClosed)return;const e=Math.min(c.RECONNECT_BASE_DELAY_MS*2**this.reconnectAttempt,c.RECONNECT_MAX_DELAY_MS);this.reconnectAttempt++,this.reconnectTimer=setTimeout(()=>{this.reconnectTimer=null,this.connect()},e)}}class u extends d{constructor(){super(...arguments);r(this,"baseUrl","");r(this,"_user",null);r(this,"_config",{...c.DEFAULT_ENV_CONFIG});r(this,"_p2pConfig",null);r(this,"_users",new Map);r(this,"_presence",new Map);r(this,"_threads",new Map);r(this,"_reactions",new Map);r(this,"_notifications",[]);r(this,"_activityLogs",[]);r(this,"_typing",new Map);r(this,"_viewports",new Map);r(this,"_selections",new Map)}get user(){return this._user}get p2pConfig(){return this._p2pConfig}get config(){return this._config}removeComment(e){for(const[t,i]of this._threads){const s=i.comments.findIndex(n=>n.id===e);if(s!==-1){i.comments.splice(s,1),i.comments.length===0&&this._threads.delete(t),this.emit("threads",this.threads);return}}}get presence(){return[...this._presence.values()]}get threads(){return[...this._threads.values()]}get notifications(){return this._notifications}get unreadCount(){return this._notifications.filter(e=>!e.read).length}markNotificationRead(e){const t=this._notifications.find(i=>i.id===e);t&&!t.read&&(t.read=!0,this.emit("notifications",this._notifications))}markAllNotificationsRead(){let e=!1;for(const t of this._notifications)t.read||(t.read=!0,e=!0);e&&this.emit("notifications",this._notifications)}get activityLogs(){return this._activityLogs}getUser(e){return this._users.get(e)}get users(){return[...this._users.values()]}getReactions(e){return this._reactions.get(e)??[]}getTypingUsers(e){const t=this._typing.get(e);if(!t)return[];const i=Date.now(),s=[];for(const[n,o]of t)i-o<3e3&&s.push(n);return s}get viewports(){return this._viewports}getViewport(e){return this._viewports.get(e)}get selections(){return this._selections}resolveUrl(e){return!this.baseUrl||!e||e.startsWith("http://")||e.startsWith("https://")?e:`${this.baseUrl}${e}`}resolveAttachments(e){return e.map(t=>({...t,url:this.resolveUrl(t.url),thumbnailUrl:t.thumbnailUrl?this.resolveUrl(t.thumbnailUrl):void 0}))}resolveComment(e){return!e.attachments||e.attachments.length===0?e:{...e,attachments:this.resolveAttachments(e.attachments)}}resolveThread(e){return{...e,comments:e.comments.map(t=>this.resolveComment(t))}}handleMessage(e){switch(e.type){case"auth:ok":this._config=e.config??{...c.DEFAULT_ENV_CONFIG},this._p2pConfig=e.p2pConfig??null,this._user=e.user,this._users.clear();for(const t of e.users)this._users.set(t.id,t);this._presence.clear();for(const t of e.presence)this._presence.set(t.user.id,t),this._users.set(t.user.id,t.user);this._users.set(e.user.id,e.user),this._threads.clear();for(const t of e.threads)this._threads.set(t.id,this.resolveThread(t));this._notifications=e.notifications,this._reactions.clear();for(const t of e.reactions){const i=this._reactions.get(t.targetId)??[];i.push(t),this._reactions.set(t.targetId,i)}this._activityLogs=[...e.activityLogs],this.emit("auth",e.user),this.emit("presence",this.presence),this.emit("threads",this.threads),this.emit("notifications",this._notifications),this.emit("reactions",null),this.emit("activity-logs",this._activityLogs);break;case"presence:join":this._presence.set(e.user.user.id,e.user),this._users.set(e.user.user.id,e.user.user),this.emit("presence",this.presence);break;case"presence:leave":this._presence.delete(e.userId),this._viewports.delete(e.userId),this._selections.delete(e.userId);for(const t of this._typing.values())t.delete(e.userId);this.emit("presence",this.presence);break;case"presence:update":{const t=this._presence.get(e.userId);t&&(t.status=e.status,this.emit("presence",this.presence));break}case"cursor:move":this.emit("cursor",{userId:e.userId,position:e.position});break;case"click:perform":this.emit("click",{userId:e.userId,position:e.position});break;case"thread:created":this._threads.set(e.thread.id,this.resolveThread(e.thread)),this.emit("threads",this.threads);break;case"comment:created":{const t=this._threads.get(e.threadId);t&&(t.comments.push(this.resolveComment(e.comment)),t.updatedAt=e.comment.createdAt,this.emit("threads",this.threads));break}case"comment:edited":{const t=this._threads.get(e.threadId);if(t){const i=t.comments.findIndex(s=>s.id===e.comment.id);i!==-1&&(t.comments[i]=this.resolveComment(e.comment)),this.emit("threads",this.threads)}break}case"comment:deleted":{const t=this._threads.get(e.threadId);t&&(t.comments=t.comments.filter(i=>i.id!==e.commentId),t.comments.length===0&&this._threads.delete(e.threadId),this.emit("threads",this.threads));break}case"thread:resolved":{const t=this._threads.get(e.threadId);t&&(t.resolved=e.resolved,this.emit("threads",this.threads));break}case"thread:deleted":this._threads.delete(e.threadId),this.emit("threads",this.threads);break;case"reaction:added":{const t=this._reactions.get(e.reaction.targetId)??[];t.push(e.reaction),this._reactions.set(e.reaction.targetId,t),this.emit("reactions",{targetId:e.reaction.targetId,reactions:t});break}case"reaction:removed":{const t=this._reactions.get(e.targetId);if(t){const i=t.filter(s=>s.id!==e.reactionId);this._reactions.set(e.targetId,i),this.emit("reactions",{targetId:e.targetId,reactions:i})}break}case"notification":this._notifications.unshift(e.notification),this.emit("notifications",this._notifications);break;case"typing:indicator":{this._typing.has(e.threadId)||this._typing.set(e.threadId,new Map),this._typing.get(e.threadId).set(e.userId,Date.now()),this.emit("typing",{threadId:e.threadId,userId:e.userId});break}case"viewport:update":{this._viewports.set(e.userId,{scrollX:e.scrollX,scrollY:e.scrollY,viewportWidth:e.viewportWidth,viewportHeight:e.viewportHeight,pageWidth:e.pageWidth,pageHeight:e.pageHeight}),this.emit("viewport",{userId:e.userId});break}case"selection:update":this._selections.set(e.userId,e.selection),this.emit("selection",{userId:e.userId,selection:e.selection});break;case"emoji:drop":this.emit("emoji-drop",{userId:e.userId,emoji:e.emoji,position:e.position});break;case"draw:stroke":this.emit("draw-stroke",{userId:e.userId,points:e.points,color:e.color,width:e.width});break;case"draw:clear":this.emit("draw-clear",{userId:e.userId});break;case"activity:logged":this._activityLogs.unshift(e.activityLog),this._activityLogs.length>100&&(this._activityLogs=this._activityLogs.slice(0,100)),this.emit("activity-logs",this._activityLogs);break;case"auth:error":this.emit("auth:error",e);break;case"error":this.emit("error",e);break}}reset(){this._user=null,this._config={...c.DEFAULT_ENV_CONFIG},this._p2pConfig=null,this._users.clear(),this._presence.clear(),this._threads.clear(),this._reactions.clear(),this._notifications=[],this._activityLogs=[],this._typing.clear(),this._viewports.clear(),this._selections.clear()}}class m extends d{constructor(e){var i;super();r(this,"state");r(this,"connection");r(this,"config");r(this,"heartbeatTimer",null);r(this,"lastCursorSend",0);r(this,"pendingCursor",null);r(this,"cursorTimer",null);r(this,"_p2p",null);r(this,"_p2pInstance",null);r(this,"p2pEnabled");r(this,"iceServers");this.config=e,this.state=new u,this.state.baseUrl=(e.endpoint??"").replace(/^ws(s?):/,"http$1:").replace(/\/$/,"");const t=((i=e.endpoint)==null?void 0:i.replace(/^http/,"ws"))??void 0;this.connection=new p(t),this.p2pEnabled=e.p2p??!1,this.iceServers=e.iceServers??[{urls:"stun:stun.l.google.com:19302"}],this.connection.on("message",s=>{this.state.handleMessage(s),this.emit(s.type,s),s.type==="auth:error"&&this.connection.permanentDisconnect(),this._p2pInstance&&(s.type==="signal:offer"?this._p2pInstance.handleSignalOffer(s.fromUserId,s.sdp):s.type==="signal:answer"?this._p2pInstance.handleSignalAnswer(s.fromUserId,s.sdp):s.type==="signal:ice"?this._p2pInstance.handleIceCandidate(s.fromUserId,s.candidate):s.type==="p2p:sync"?this._p2pInstance.handleP2PSync(s.fromUserId,s.update):s.type==="presence:join"?this._p2pInstance.onPeerJoined(s.user.user.id):s.type==="presence:leave"&&this._p2pInstance.onPeerLeft(s.userId))}),this.connection.on("state",s=>{this.emit("connection",s),s==="connected"?(this.authenticate(),this.startHeartbeat()):s==="disconnected"&&this.stopHeartbeat()})}get connectionState(){return this.connection.state}connect(){this.connection.connect()}disconnect(){this.stopHeartbeat(),this.cursorTimer&&(clearTimeout(this.cursorTimer),this.cursorTimer=null),this.connection.disconnect(),this.state.reset()}destroy(){var e;(e=this._p2pInstance)==null||e.destroy(),this._p2pInstance=null,this._p2p=null,this.disconnect(),this.removeAll(),this.state.removeAll(),this.connection.removeAll()}authenticate(){this.send({type:"auth",apiKey:this.config.apiKey,token:this.config.token,room:this.config.room})}send(e){this.connection.send(e)}moveCursor(e){const t=Date.now();this.pendingCursor=e,t-this.lastCursorSend>=c.CURSOR_THROTTLE_MS?this.flushCursor():this.cursorTimer||(this.cursorTimer=setTimeout(()=>{this.cursorTimer=null,this.flushCursor()},c.CURSOR_THROTTLE_MS))}flushCursor(){this.pendingCursor&&(this.send({type:"cursor:move",position:this.pendingCursor}),this.lastCursorSend=Date.now(),this.pendingCursor=null)}updatePresence(e){this.send({type:"presence:update",status:e})}startHeartbeat(){this.heartbeatTimer||(this.heartbeatTimer=setInterval(()=>{this.send({type:"presence:update",status:"online"})},c.PRESENCE_HEARTBEAT_MS))}stopHeartbeat(){this.heartbeatTimer&&(clearInterval(this.heartbeatTimer),this.heartbeatTimer=null)}createThread(e,t={}){const i=crypto.randomUUID();return this.send({type:"thread:create",id:i,body:e,mentions:t.mentions??[],position:t.position??null,attachmentIds:t.attachmentIds}),i}reply(e,t,i=[],s){const n=crypto.randomUUID();return this.send({type:"comment:create",threadId:e,id:n,body:t,mentions:i,attachmentIds:s}),n}editComment(e,t,i=[]){this.send({type:"comment:edit",commentId:e,body:t,mentions:i})}deleteComment(e){this.state.removeComment(e),this.send({type:"comment:delete",commentId:e})}resolveThread(e,t=!0){this.send({type:"thread:resolve",threadId:e,resolved:t})}addReaction(e,t,i){this.send({type:"reaction:add",targetId:e,targetType:t,emoji:i})}removeReaction(e){this.send({type:"reaction:remove",reactionId:e})}markRead(e){this.state.markNotificationRead(e),this.send({type:"notification:read",notificationId:e})}markAllRead(){this.state.markAllNotificationsRead(),this.send({type:"notification:read-all"})}performClick(e){this.send({type:"click:perform",position:e})}sendTyping(e){this.send({type:"typing:start",threadId:e})}updateViewport(e){this.send({type:"viewport:update",...e})}updateSelection(e){this.send({type:"selection:update",selection:e})}dropEmoji(e,t){this.send({type:"emoji:drop",emoji:e,position:t})}drawStroke(e,t,i){this.send({type:"draw:stroke",points:e,color:t,width:i})}clearDrawing(){this.send({type:"draw:clear"})}async uploadFile(e){const t=typeof window<"u"?window.location.origin:"",i=(this.config.endpoint??t).replace(/^ws(s?):/,"http$1:"),s=new FormData;s.append("file",e);const n=await fetch(`${i}/api/v1/upload`,{method:"POST",headers:{"X-Pulse-Key":this.config.apiKey,"X-Pulse-Token":this.config.token},body:s});if(!n.ok){const l=await n.json().catch(()=>({error:"Upload failed"}));throw new Error(l.error??"Upload failed")}const o=await n.json();return o.url&&!o.url.startsWith("http")&&(o.url=`${i}${o.url}`),o.thumbnailUrl&&!o.thumbnailUrl.startsWith("http")&&(o.thumbnailUrl=`${i}${o.thumbnailUrl}`),o}setAppearOffline(e){e?(this.stopHeartbeat(),this.send({type:"presence:update",status:"idle"})):(this.startHeartbeat(),this.send({type:"presence:update",status:"online"}))}get p2p(){return this.p2pEnabled?(this._p2p||(this._p2p=this.initP2P()),this._p2p):Promise.reject(new Error("P2P is not enabled. Pass { p2p: true } in PulseConfig."))}async initP2P(){const e=await new Promise((n,o)=>{if(this.state.user&&this.state.p2pConfig){n({userId:this.state.user.id,p2pConfig:this.state.p2pConfig});return}const l=this.state.on("auth",()=>{if(l(),!this.state.p2pConfig){o(new Error("P2P is not enabled for this environment."));return}n({userId:this.state.user.id,p2pConfig:this.state.p2pConfig})})});let t=this.iceServers;try{const n=await fetch(`${this.state.baseUrl}/api/v1/turn-credentials`,{headers:{Authorization:`Bearer ${this.config.token}`}});n.ok&&(t=(await n.json()).iceServers)}catch{}const{P2PManager:i}=await Promise.resolve().then(()=>require("./P2PManager-e4ShL60A.cjs")),s=new i({roomId:this.config.room,userId:e.userId,baseUrl:this.state.baseUrl,authToken:this.config.token,p2pConfig:e.p2pConfig,iceServers:t,sendWS:n=>this.send(n)});this._p2pInstance=s,await s.initialize();for(const n of this.state.presence)n.user.id!==e.userId&&s.onPeerJoined(n.user.id);return s}}exports.Connection=p;exports.Emitter=d;exports.PulseClient=m;exports.StateManager=u;
1
+ "use strict";var f=Object.defineProperty;var _=(h,a,e)=>a in h?f(h,a,{enumerable:!0,configurable:!0,writable:!0,value:e}):h[a]=e;var r=(h,a,e)=>_(h,typeof a!="symbol"?a+"":a,e);Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const c=require("@gamention/pulse-shared");class d{constructor(){r(this,"handlers",new Map)}on(a,e){this.handlers.has(a)||this.handlers.set(a,new Set);const t=this.handlers.get(a);return t.add(e),()=>t.delete(e)}off(a,e){var t;(t=this.handlers.get(a))==null||t.delete(e)}emit(a,e){var t;(t=this.handlers.get(a))==null||t.forEach(s=>s(e))}removeAll(){this.handlers.clear()}}class p extends d{constructor(e){super();r(this,"ws",null);r(this,"endpoint");r(this,"reconnectAttempt",0);r(this,"reconnectTimer",null);r(this,"_state","disconnected");r(this,"permanentlyClosed",!1);r(this,"handleOpen",()=>{this._state="connected",this.reconnectAttempt=0,this.emit("state",this._state)});r(this,"handleMessage",e=>{try{const t=JSON.parse(e.data);this.emit("message",t)}catch{}});r(this,"handleClose",()=>{this.cleanupWs(),this._state="disconnected",this.emit("state",this._state),this.scheduleReconnect()});r(this,"handleError",()=>{var e;(e=this.ws)==null||e.close()});this.endpoint=e??c.DEFAULT_ENDPOINT}get state(){return this._state}connect(){this.ws||this.permanentlyClosed||(this._state="connecting",this.emit("state",this._state),this.ws=new WebSocket(this.endpoint),this.ws.addEventListener("open",this.handleOpen),this.ws.addEventListener("message",this.handleMessage),this.ws.addEventListener("close",this.handleClose),this.ws.addEventListener("error",this.handleError))}disconnect(){this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.reconnectAttempt=0,this.cleanupWs(),this._state="disconnected",this.emit("state",this._state)}send(e){var t;((t=this.ws)==null?void 0:t.readyState)===WebSocket.OPEN&&this.ws.send(JSON.stringify(e))}permanentDisconnect(){this.permanentlyClosed=!0,this.disconnect()}cleanupWs(){this.ws&&(this.ws.removeEventListener("open",this.handleOpen),this.ws.removeEventListener("message",this.handleMessage),this.ws.removeEventListener("close",this.handleClose),this.ws.removeEventListener("error",this.handleError),this.ws.close(),this.ws=null)}scheduleReconnect(){if(this.permanentlyClosed)return;const e=Math.min(c.RECONNECT_BASE_DELAY_MS*2**this.reconnectAttempt,c.RECONNECT_MAX_DELAY_MS);this.reconnectAttempt++,this.reconnectTimer=setTimeout(()=>{this.reconnectTimer=null,this.connect()},e)}}class u extends d{constructor(){super(...arguments);r(this,"baseUrl","");r(this,"_user",null);r(this,"_config",{...c.DEFAULT_ENV_CONFIG});r(this,"_p2pConfig",null);r(this,"_users",new Map);r(this,"_presence",new Map);r(this,"_threads",new Map);r(this,"_reactions",new Map);r(this,"_notifications",[]);r(this,"_activityLogs",[]);r(this,"_typing",new Map);r(this,"_viewports",new Map);r(this,"_selections",new Map)}get user(){return this._user}get p2pConfig(){return this._p2pConfig}get config(){return this._config}removeComment(e){for(const[t,s]of this._threads){const i=s.comments.findIndex(n=>n.id===e);if(i!==-1){s.comments.splice(i,1),s.comments.length===0&&this._threads.delete(t),this.emit("threads",this.threads);return}}}get presence(){return[...this._presence.values()]}get threads(){return[...this._threads.values()]}get notifications(){return this._notifications}get unreadCount(){return this._notifications.filter(e=>!e.read).length}markNotificationRead(e){const t=this._notifications.find(s=>s.id===e);t&&!t.read&&(t.read=!0,this.emit("notifications",this._notifications))}markAllNotificationsRead(){let e=!1;for(const t of this._notifications)t.read||(t.read=!0,e=!0);e&&this.emit("notifications",this._notifications)}get activityLogs(){return this._activityLogs}getUser(e){return this._users.get(e)}get users(){return[...this._users.values()]}getReactions(e){return this._reactions.get(e)??[]}getTypingUsers(e){const t=this._typing.get(e);if(!t)return[];const s=Date.now(),i=[];for(const[n,o]of t)s-o<3e3&&i.push(n);return i}get viewports(){return this._viewports}getViewport(e){return this._viewports.get(e)}get selections(){return this._selections}resolveUrl(e){return!this.baseUrl||!e||e.startsWith("http://")||e.startsWith("https://")?e:`${this.baseUrl}${e}`}resolveAttachments(e){return e.map(t=>({...t,url:this.resolveUrl(t.url),thumbnailUrl:t.thumbnailUrl?this.resolveUrl(t.thumbnailUrl):void 0}))}resolveComment(e){return!e.attachments||e.attachments.length===0?e:{...e,attachments:this.resolveAttachments(e.attachments)}}resolveThread(e){return{...e,comments:e.comments.map(t=>this.resolveComment(t))}}handleMessage(e){switch(e.type){case"auth:ok":this._config=e.config??{...c.DEFAULT_ENV_CONFIG},this._p2pConfig=e.p2pConfig??null,this._user=e.user,this._users.clear();for(const t of e.users)this._users.set(t.id,t);this._presence.clear();for(const t of e.presence)this._presence.set(t.user.id,t),this._users.set(t.user.id,t.user);this._users.set(e.user.id,e.user),this._threads.clear();for(const t of e.threads)this._threads.set(t.id,this.resolveThread(t));this._notifications=e.notifications,this._reactions.clear();for(const t of e.reactions){const s=this._reactions.get(t.targetId)??[];s.push(t),this._reactions.set(t.targetId,s)}this._activityLogs=[...e.activityLogs],this.emit("auth",e.user),this.emit("presence",this.presence),this.emit("threads",this.threads),this.emit("notifications",this._notifications),this.emit("reactions",null),this.emit("activity-logs",this._activityLogs);break;case"presence:join":this._presence.set(e.user.user.id,e.user),this._users.set(e.user.user.id,e.user.user),this.emit("presence",this.presence);break;case"presence:leave":this._presence.delete(e.userId),this._viewports.delete(e.userId),this._selections.delete(e.userId);for(const t of this._typing.values())t.delete(e.userId);this.emit("presence",this.presence);break;case"presence:update":{const t=this._presence.get(e.userId);t&&(t.status=e.status,this.emit("presence",this.presence));break}case"cursor:move":this.emit("cursor",{userId:e.userId,position:e.position});break;case"click:perform":this.emit("click",{userId:e.userId,position:e.position});break;case"thread:created":this._threads.set(e.thread.id,this.resolveThread(e.thread)),this.emit("threads",this.threads);break;case"comment:created":{const t=this._threads.get(e.threadId);t&&(t.comments.push(this.resolveComment(e.comment)),t.updatedAt=e.comment.createdAt,this.emit("threads",this.threads));break}case"comment:edited":{const t=this._threads.get(e.threadId);if(t){const s=t.comments.findIndex(i=>i.id===e.comment.id);s!==-1&&(t.comments[s]=this.resolveComment(e.comment)),this.emit("threads",this.threads)}break}case"comment:deleted":{const t=this._threads.get(e.threadId);t&&(t.comments=t.comments.filter(s=>s.id!==e.commentId),t.comments.length===0&&this._threads.delete(e.threadId),this.emit("threads",this.threads));break}case"thread:resolved":{const t=this._threads.get(e.threadId);t&&(t.resolved=e.resolved,this.emit("threads",this.threads));break}case"thread:deleted":this._threads.delete(e.threadId),this.emit("threads",this.threads);break;case"reaction:added":{const t=this._reactions.get(e.reaction.targetId)??[];t.push(e.reaction),this._reactions.set(e.reaction.targetId,t),this.emit("reactions",{targetId:e.reaction.targetId,reactions:t});break}case"reaction:removed":{const t=this._reactions.get(e.targetId);if(t){const s=t.filter(i=>i.id!==e.reactionId);this._reactions.set(e.targetId,s),this.emit("reactions",{targetId:e.targetId,reactions:s})}break}case"notification":this._notifications.unshift(e.notification),this.emit("notifications",this._notifications);break;case"typing:indicator":{this._typing.has(e.threadId)||this._typing.set(e.threadId,new Map),this._typing.get(e.threadId).set(e.userId,Date.now()),this.emit("typing",{threadId:e.threadId,userId:e.userId});break}case"viewport:update":{this._viewports.set(e.userId,{scrollX:e.scrollX,scrollY:e.scrollY,viewportWidth:e.viewportWidth,viewportHeight:e.viewportHeight,pageWidth:e.pageWidth,pageHeight:e.pageHeight}),this.emit("viewport",{userId:e.userId});break}case"selection:update":this._selections.set(e.userId,e.selection),this.emit("selection",{userId:e.userId,selection:e.selection});break;case"emoji:drop":this.emit("emoji-drop",{userId:e.userId,emoji:e.emoji,position:e.position});break;case"draw:stroke":this.emit("draw-stroke",{userId:e.userId,points:e.points,color:e.color,width:e.width});break;case"draw:clear":this.emit("draw-clear",{userId:e.userId});break;case"activity:logged":this._activityLogs.unshift(e.activityLog),this._activityLogs.length>100&&(this._activityLogs=this._activityLogs.slice(0,100)),this.emit("activity-logs",this._activityLogs);break;case"auth:error":this.emit("auth:error",e);break;case"error":this.emit("error",e);break}}reset(){this._user=null,this._config={...c.DEFAULT_ENV_CONFIG},this._p2pConfig=null,this._users.clear(),this._presence.clear(),this._threads.clear(),this._reactions.clear(),this._notifications=[],this._activityLogs=[],this._typing.clear(),this._viewports.clear(),this._selections.clear()}}class m extends d{constructor(e){var s;super();r(this,"state");r(this,"connection");r(this,"config");r(this,"heartbeatTimer",null);r(this,"lastCursorSend",0);r(this,"pendingCursor",null);r(this,"cursorTimer",null);r(this,"_p2p",null);r(this,"_p2pInstance",null);r(this,"p2pEnabled");r(this,"iceServers");r(this,"signalingQueue",[]);this.config=e,this.state=new u,this.state.baseUrl=(e.endpoint??"").replace(/^ws(s?):/,"http$1:").replace(/\/$/,"");const t=((s=e.endpoint)==null?void 0:s.replace(/^http/,"ws"))??void 0;this.connection=new p(t),this.p2pEnabled=e.p2p??!1,this.iceServers=e.iceServers??[{urls:"stun:stun.l.google.com:19302"}],this.connection.on("message",i=>{this.state.handleMessage(i),this.emit(i.type,i),i.type==="auth:error"&&this.connection.permanentDisconnect(),i.type==="signal:offer"||i.type==="signal:answer"||i.type==="signal:ice"||i.type==="p2p:sync"?this._p2pInstance?this.routeP2PMessage(i):this.p2pEnabled&&this.signalingQueue.push(i):this._p2pInstance&&(i.type==="presence:join"?this._p2pInstance.onPeerJoined(i.user.user.id):i.type==="presence:leave"&&this._p2pInstance.onPeerLeft(i.userId))}),this.connection.on("state",i=>{this.emit("connection",i),i==="connected"?(this.authenticate(),this.startHeartbeat()):i==="disconnected"&&this.stopHeartbeat()})}get connectionState(){return this.connection.state}connect(){this.connection.connect()}disconnect(){this.stopHeartbeat(),this.cursorTimer&&(clearTimeout(this.cursorTimer),this.cursorTimer=null),this.connection.disconnect(),this.state.reset()}destroy(){var e;(e=this._p2pInstance)==null||e.destroy(),this._p2pInstance=null,this._p2p=null,this.disconnect(),this.removeAll(),this.state.removeAll(),this.connection.removeAll()}authenticate(){this.send({type:"auth",apiKey:this.config.apiKey,token:this.config.token,room:this.config.room})}send(e){this.connection.send(e)}moveCursor(e){const t=Date.now();this.pendingCursor=e,t-this.lastCursorSend>=c.CURSOR_THROTTLE_MS?this.flushCursor():this.cursorTimer||(this.cursorTimer=setTimeout(()=>{this.cursorTimer=null,this.flushCursor()},c.CURSOR_THROTTLE_MS))}flushCursor(){this.pendingCursor&&(this.send({type:"cursor:move",position:this.pendingCursor}),this.lastCursorSend=Date.now(),this.pendingCursor=null)}updatePresence(e){this.send({type:"presence:update",status:e})}startHeartbeat(){this.heartbeatTimer||(this.heartbeatTimer=setInterval(()=>{this.send({type:"presence:update",status:"online"})},c.PRESENCE_HEARTBEAT_MS))}stopHeartbeat(){this.heartbeatTimer&&(clearInterval(this.heartbeatTimer),this.heartbeatTimer=null)}createThread(e,t={}){const s=crypto.randomUUID();return this.send({type:"thread:create",id:s,body:e,mentions:t.mentions??[],position:t.position??null,attachmentIds:t.attachmentIds}),s}reply(e,t,s=[],i){const n=crypto.randomUUID();return this.send({type:"comment:create",threadId:e,id:n,body:t,mentions:s,attachmentIds:i}),n}editComment(e,t,s=[]){this.send({type:"comment:edit",commentId:e,body:t,mentions:s})}deleteComment(e){this.state.removeComment(e),this.send({type:"comment:delete",commentId:e})}resolveThread(e,t=!0){this.send({type:"thread:resolve",threadId:e,resolved:t})}addReaction(e,t,s){this.send({type:"reaction:add",targetId:e,targetType:t,emoji:s})}removeReaction(e){this.send({type:"reaction:remove",reactionId:e})}markRead(e){this.state.markNotificationRead(e),this.send({type:"notification:read",notificationId:e})}markAllRead(){this.state.markAllNotificationsRead(),this.send({type:"notification:read-all"})}performClick(e){this.send({type:"click:perform",position:e})}sendTyping(e){this.send({type:"typing:start",threadId:e})}updateViewport(e){this.send({type:"viewport:update",...e})}updateSelection(e){this.send({type:"selection:update",selection:e})}dropEmoji(e,t){this.send({type:"emoji:drop",emoji:e,position:t})}drawStroke(e,t,s){this.send({type:"draw:stroke",points:e,color:t,width:s})}clearDrawing(){this.send({type:"draw:clear"})}async uploadFile(e){const t=typeof window<"u"?window.location.origin:"",s=(this.config.endpoint??t).replace(/^ws(s?):/,"http$1:"),i=new FormData;i.append("file",e);const n=await fetch(`${s}/api/v1/upload`,{method:"POST",headers:{"X-Pulse-Key":this.config.apiKey,"X-Pulse-Token":this.config.token},body:i});if(!n.ok){const l=await n.json().catch(()=>({error:"Upload failed"}));throw new Error(l.error??"Upload failed")}const o=await n.json();return o.url&&!o.url.startsWith("http")&&(o.url=`${s}${o.url}`),o.thumbnailUrl&&!o.thumbnailUrl.startsWith("http")&&(o.thumbnailUrl=`${s}${o.thumbnailUrl}`),o}setAppearOffline(e){e?(this.stopHeartbeat(),this.send({type:"presence:update",status:"idle"})):(this.startHeartbeat(),this.send({type:"presence:update",status:"online"}))}get p2p(){return this.p2pEnabled?(this._p2p||(this._p2p=this.initP2P()),this._p2p):Promise.reject(new Error("P2P is not enabled. Pass { p2p: true } in PulseConfig."))}async initP2P(){const e=await new Promise((n,o)=>{if(this.state.user&&this.state.p2pConfig){n({userId:this.state.user.id,p2pConfig:this.state.p2pConfig});return}const l=this.state.on("auth",()=>{if(l(),!this.state.p2pConfig){o(new Error("P2P is not enabled for this environment."));return}n({userId:this.state.user.id,p2pConfig:this.state.p2pConfig})})});let t=this.iceServers;try{const n=await fetch(`${this.state.baseUrl}/api/v1/turn-credentials`,{headers:{Authorization:`Bearer ${this.config.token}`}});n.ok&&(t=(await n.json()).iceServers)}catch{}const{P2PManager:s}=await Promise.resolve().then(()=>require("./P2PManager-e4ShL60A.cjs")),i=new s({roomId:this.config.room,userId:e.userId,baseUrl:this.state.baseUrl,authToken:this.config.token,p2pConfig:e.p2pConfig,iceServers:t,sendWS:n=>this.send(n)});this._p2pInstance=i,await i.initialize();for(const n of this.state.presence)n.user.id!==e.userId&&i.onPeerJoined(n.user.id);for(const n of this.signalingQueue)this.routeP2PMessage(n);return this.signalingQueue=[],i}routeP2PMessage(e){this._p2pInstance&&(e.type==="signal:offer"?this._p2pInstance.handleSignalOffer(e.fromUserId,e.sdp):e.type==="signal:answer"?this._p2pInstance.handleSignalAnswer(e.fromUserId,e.sdp):e.type==="signal:ice"?this._p2pInstance.handleIceCandidate(e.fromUserId,e.candidate):e.type==="p2p:sync"&&this._p2pInstance.handleP2PSync(e.fromUserId,e.update))}}exports.Connection=p;exports.Emitter=d;exports.PulseClient=m;exports.StateManager=u;
@@ -1,8 +1,8 @@
1
1
  var u = Object.defineProperty;
2
2
  var f = (h, a, e) => a in h ? u(h, a, { enumerable: !0, configurable: !0, writable: !0, value: e }) : h[a] = e;
3
3
  var r = (h, a, e) => f(h, typeof a != "symbol" ? a + "" : a, e);
4
- import { DEFAULT_ENDPOINT as _, RECONNECT_BASE_DELAY_MS as m, RECONNECT_MAX_DELAY_MS as w, DEFAULT_ENV_CONFIG as d, CURSOR_THROTTLE_MS as l, PRESENCE_HEARTBEAT_MS as y } from "@gamention/pulse-shared";
5
- class p {
4
+ import { DEFAULT_ENDPOINT as _, RECONNECT_BASE_DELAY_MS as m, RECONNECT_MAX_DELAY_MS as w, DEFAULT_ENV_CONFIG as d, CURSOR_THROTTLE_MS as p, PRESENCE_HEARTBEAT_MS as v } from "@gamention/pulse-shared";
5
+ class l {
6
6
  constructor() {
7
7
  r(this, "handlers", /* @__PURE__ */ new Map());
8
8
  }
@@ -17,13 +17,13 @@ class p {
17
17
  }
18
18
  emit(a, e) {
19
19
  var t;
20
- (t = this.handlers.get(a)) == null || t.forEach((i) => i(e));
20
+ (t = this.handlers.get(a)) == null || t.forEach((s) => s(e));
21
21
  }
22
22
  removeAll() {
23
23
  this.handlers.clear();
24
24
  }
25
25
  }
26
- class v extends p {
26
+ class y extends l {
27
27
  constructor(e) {
28
28
  super();
29
29
  r(this, "ws", null);
@@ -32,30 +32,34 @@ class v extends p {
32
32
  r(this, "reconnectTimer", null);
33
33
  r(this, "_state", "disconnected");
34
34
  r(this, "permanentlyClosed", !1);
35
- this.endpoint = e ?? _;
36
- }
37
- get state() {
38
- return this._state;
39
- }
40
- connect() {
41
- this.ws || this.permanentlyClosed || (this._state = "connecting", this.emit("state", this._state), this.ws = new WebSocket(this.endpoint), this.ws.addEventListener("open", () => {
35
+ // Stored handlers for proper cleanup
36
+ r(this, "handleOpen", () => {
42
37
  this._state = "connected", this.reconnectAttempt = 0, this.emit("state", this._state);
43
- }), this.ws.addEventListener("message", (e) => {
38
+ });
39
+ r(this, "handleMessage", (e) => {
44
40
  try {
45
41
  const t = JSON.parse(e.data);
46
42
  this.emit("message", t);
47
43
  } catch {
48
44
  }
49
- }), this.ws.addEventListener("close", () => {
50
- this.ws = null, this._state = "disconnected", this.emit("state", this._state), this.scheduleReconnect();
51
- }), this.ws.addEventListener("error", () => {
45
+ });
46
+ r(this, "handleClose", () => {
47
+ this.cleanupWs(), this._state = "disconnected", this.emit("state", this._state), this.scheduleReconnect();
48
+ });
49
+ r(this, "handleError", () => {
52
50
  var e;
53
51
  (e = this.ws) == null || e.close();
54
- }));
52
+ });
53
+ this.endpoint = e ?? _;
54
+ }
55
+ get state() {
56
+ return this._state;
57
+ }
58
+ connect() {
59
+ this.ws || this.permanentlyClosed || (this._state = "connecting", this.emit("state", this._state), this.ws = new WebSocket(this.endpoint), this.ws.addEventListener("open", this.handleOpen), this.ws.addEventListener("message", this.handleMessage), this.ws.addEventListener("close", this.handleClose), this.ws.addEventListener("error", this.handleError));
55
60
  }
56
61
  disconnect() {
57
- var e;
58
- this.reconnectTimer && (clearTimeout(this.reconnectTimer), this.reconnectTimer = null), this.reconnectAttempt = 0, (e = this.ws) == null || e.close(), this.ws = null, this._state = "disconnected", this.emit("state", this._state);
62
+ this.reconnectTimer && (clearTimeout(this.reconnectTimer), this.reconnectTimer = null), this.reconnectAttempt = 0, this.cleanupWs(), this._state = "disconnected", this.emit("state", this._state);
59
63
  }
60
64
  send(e) {
61
65
  var t;
@@ -64,6 +68,9 @@ class v extends p {
64
68
  permanentDisconnect() {
65
69
  this.permanentlyClosed = !0, this.disconnect();
66
70
  }
71
+ cleanupWs() {
72
+ this.ws && (this.ws.removeEventListener("open", this.handleOpen), this.ws.removeEventListener("message", this.handleMessage), this.ws.removeEventListener("close", this.handleClose), this.ws.removeEventListener("error", this.handleError), this.ws.close(), this.ws = null);
73
+ }
67
74
  scheduleReconnect() {
68
75
  if (this.permanentlyClosed) return;
69
76
  const e = Math.min(
@@ -75,7 +82,7 @@ class v extends p {
75
82
  }, e);
76
83
  }
77
84
  }
78
- class I extends p {
85
+ class I extends l {
79
86
  constructor() {
80
87
  super(...arguments);
81
88
  /** HTTP base URL for resolving relative attachment paths (e.g. "http://localhost:4000"). */
@@ -104,10 +111,10 @@ class I extends p {
104
111
  }
105
112
  /** Optimistically remove a comment from local state (before server round-trip). */
106
113
  removeComment(e) {
107
- for (const [t, i] of this._threads) {
108
- const s = i.comments.findIndex((n) => n.id === e);
109
- if (s !== -1) {
110
- i.comments.splice(s, 1), i.comments.length === 0 && this._threads.delete(t), this.emit("threads", this.threads);
114
+ for (const [t, s] of this._threads) {
115
+ const i = s.comments.findIndex((n) => n.id === e);
116
+ if (i !== -1) {
117
+ s.comments.splice(i, 1), s.comments.length === 0 && this._threads.delete(t), this.emit("threads", this.threads);
111
118
  return;
112
119
  }
113
120
  }
@@ -126,7 +133,7 @@ class I extends p {
126
133
  }
127
134
  /** Optimistically mark a single notification as read. */
128
135
  markNotificationRead(e) {
129
- const t = this._notifications.find((i) => i.id === e);
136
+ const t = this._notifications.find((s) => s.id === e);
130
137
  t && !t.read && (t.read = !0, this.emit("notifications", this._notifications));
131
138
  }
132
139
  /** Optimistically mark all notifications as read. */
@@ -152,10 +159,10 @@ class I extends p {
152
159
  getTypingUsers(e) {
153
160
  const t = this._typing.get(e);
154
161
  if (!t) return [];
155
- const i = Date.now(), s = [];
162
+ const s = Date.now(), i = [];
156
163
  for (const [n, o] of t)
157
- i - o < 3e3 && s.push(n);
158
- return s;
164
+ s - o < 3e3 && i.push(n);
165
+ return i;
159
166
  }
160
167
  get viewports() {
161
168
  return this._viewports;
@@ -198,8 +205,8 @@ class I extends p {
198
205
  for (const t of e.threads) this._threads.set(t.id, this.resolveThread(t));
199
206
  this._notifications = e.notifications, this._reactions.clear();
200
207
  for (const t of e.reactions) {
201
- const i = this._reactions.get(t.targetId) ?? [];
202
- i.push(t), this._reactions.set(t.targetId, i);
208
+ const s = this._reactions.get(t.targetId) ?? [];
209
+ s.push(t), this._reactions.set(t.targetId, s);
203
210
  }
204
211
  this._activityLogs = [...e.activityLogs], this.emit("auth", e.user), this.emit("presence", this.presence), this.emit("threads", this.threads), this.emit("notifications", this._notifications), this.emit("reactions", null), this.emit("activity-logs", this._activityLogs);
205
212
  break;
@@ -234,14 +241,14 @@ class I extends p {
234
241
  case "comment:edited": {
235
242
  const t = this._threads.get(e.threadId);
236
243
  if (t) {
237
- const i = t.comments.findIndex((s) => s.id === e.comment.id);
238
- i !== -1 && (t.comments[i] = this.resolveComment(e.comment)), this.emit("threads", this.threads);
244
+ const s = t.comments.findIndex((i) => i.id === e.comment.id);
245
+ s !== -1 && (t.comments[s] = this.resolveComment(e.comment)), this.emit("threads", this.threads);
239
246
  }
240
247
  break;
241
248
  }
242
249
  case "comment:deleted": {
243
250
  const t = this._threads.get(e.threadId);
244
- t && (t.comments = t.comments.filter((i) => i.id !== e.commentId), t.comments.length === 0 && this._threads.delete(e.threadId), this.emit("threads", this.threads));
251
+ t && (t.comments = t.comments.filter((s) => s.id !== e.commentId), t.comments.length === 0 && this._threads.delete(e.threadId), this.emit("threads", this.threads));
245
252
  break;
246
253
  }
247
254
  case "thread:resolved": {
@@ -263,10 +270,10 @@ class I extends p {
263
270
  case "reaction:removed": {
264
271
  const t = this._reactions.get(e.targetId);
265
272
  if (t) {
266
- const i = t.filter((s) => s.id !== e.reactionId);
267
- this._reactions.set(e.targetId, i), this.emit("reactions", {
273
+ const s = t.filter((i) => i.id !== e.reactionId);
274
+ this._reactions.set(e.targetId, s), this.emit("reactions", {
268
275
  targetId: e.targetId,
269
- reactions: i
276
+ reactions: s
270
277
  });
271
278
  }
272
279
  break;
@@ -325,9 +332,9 @@ class I extends p {
325
332
  this._user = null, this._config = { ...d }, this._p2pConfig = null, this._users.clear(), this._presence.clear(), this._threads.clear(), this._reactions.clear(), this._notifications = [], this._activityLogs = [], this._typing.clear(), this._viewports.clear(), this._selections.clear();
326
333
  }
327
334
  }
328
- class k extends p {
335
+ class C extends l {
329
336
  constructor(e) {
330
- var i;
337
+ var s;
331
338
  super();
332
339
  r(this, "state");
333
340
  r(this, "connection");
@@ -340,12 +347,13 @@ class k extends p {
340
347
  r(this, "_p2pInstance", null);
341
348
  r(this, "p2pEnabled");
342
349
  r(this, "iceServers");
350
+ r(this, "signalingQueue", []);
343
351
  this.config = e, this.state = new I(), this.state.baseUrl = (e.endpoint ?? "").replace(/^ws(s?):/, "http$1:").replace(/\/$/, "");
344
- const t = ((i = e.endpoint) == null ? void 0 : i.replace(/^http/, "ws")) ?? void 0;
345
- this.connection = new v(t), this.p2pEnabled = e.p2p ?? !1, this.iceServers = e.iceServers ?? [{ urls: "stun:stun.l.google.com:19302" }], this.connection.on("message", (s) => {
346
- this.state.handleMessage(s), this.emit(s.type, s), s.type === "auth:error" && this.connection.permanentDisconnect(), this._p2pInstance && (s.type === "signal:offer" ? this._p2pInstance.handleSignalOffer(s.fromUserId, s.sdp) : s.type === "signal:answer" ? this._p2pInstance.handleSignalAnswer(s.fromUserId, s.sdp) : s.type === "signal:ice" ? this._p2pInstance.handleIceCandidate(s.fromUserId, s.candidate) : s.type === "p2p:sync" ? this._p2pInstance.handleP2PSync(s.fromUserId, s.update) : s.type === "presence:join" ? this._p2pInstance.onPeerJoined(s.user.user.id) : s.type === "presence:leave" && this._p2pInstance.onPeerLeft(s.userId));
347
- }), this.connection.on("state", (s) => {
348
- this.emit("connection", s), s === "connected" ? (this.authenticate(), this.startHeartbeat()) : s === "disconnected" && this.stopHeartbeat();
352
+ const t = ((s = e.endpoint) == null ? void 0 : s.replace(/^http/, "ws")) ?? void 0;
353
+ this.connection = new y(t), this.p2pEnabled = e.p2p ?? !1, this.iceServers = e.iceServers ?? [{ urls: "stun:stun.l.google.com:19302" }], this.connection.on("message", (i) => {
354
+ this.state.handleMessage(i), this.emit(i.type, i), i.type === "auth:error" && this.connection.permanentDisconnect(), i.type === "signal:offer" || i.type === "signal:answer" || i.type === "signal:ice" || i.type === "p2p:sync" ? this._p2pInstance ? this.routeP2PMessage(i) : this.p2pEnabled && this.signalingQueue.push(i) : this._p2pInstance && (i.type === "presence:join" ? this._p2pInstance.onPeerJoined(i.user.user.id) : i.type === "presence:leave" && this._p2pInstance.onPeerLeft(i.userId));
355
+ }), this.connection.on("state", (i) => {
356
+ this.emit("connection", i), i === "connected" ? (this.authenticate(), this.startHeartbeat()) : i === "disconnected" && this.stopHeartbeat();
349
357
  });
350
358
  }
351
359
  /** Current WebSocket connection state. */
@@ -376,9 +384,9 @@ class k extends p {
376
384
  // ── Cursors ──
377
385
  moveCursor(e) {
378
386
  const t = Date.now();
379
- this.pendingCursor = e, t - this.lastCursorSend >= l ? this.flushCursor() : this.cursorTimer || (this.cursorTimer = setTimeout(() => {
387
+ this.pendingCursor = e, t - this.lastCursorSend >= p ? this.flushCursor() : this.cursorTimer || (this.cursorTimer = setTimeout(() => {
380
388
  this.cursorTimer = null, this.flushCursor();
381
- }, l));
389
+ }, p));
382
390
  }
383
391
  flushCursor() {
384
392
  this.pendingCursor && (this.send({ type: "cursor:move", position: this.pendingCursor }), this.lastCursorSend = Date.now(), this.pendingCursor = null);
@@ -390,29 +398,29 @@ class k extends p {
390
398
  startHeartbeat() {
391
399
  this.heartbeatTimer || (this.heartbeatTimer = setInterval(() => {
392
400
  this.send({ type: "presence:update", status: "online" });
393
- }, y));
401
+ }, v));
394
402
  }
395
403
  stopHeartbeat() {
396
404
  this.heartbeatTimer && (clearInterval(this.heartbeatTimer), this.heartbeatTimer = null);
397
405
  }
398
406
  // ── Threads & Comments ──
399
407
  createThread(e, t = {}) {
400
- const i = crypto.randomUUID();
408
+ const s = crypto.randomUUID();
401
409
  return this.send({
402
410
  type: "thread:create",
403
- id: i,
411
+ id: s,
404
412
  body: e,
405
413
  mentions: t.mentions ?? [],
406
414
  position: t.position ?? null,
407
415
  attachmentIds: t.attachmentIds
408
- }), i;
416
+ }), s;
409
417
  }
410
- reply(e, t, i = [], s) {
418
+ reply(e, t, s = [], i) {
411
419
  const n = crypto.randomUUID();
412
- return this.send({ type: "comment:create", threadId: e, id: n, body: t, mentions: i, attachmentIds: s }), n;
420
+ return this.send({ type: "comment:create", threadId: e, id: n, body: t, mentions: s, attachmentIds: i }), n;
413
421
  }
414
- editComment(e, t, i = []) {
415
- this.send({ type: "comment:edit", commentId: e, body: t, mentions: i });
422
+ editComment(e, t, s = []) {
423
+ this.send({ type: "comment:edit", commentId: e, body: t, mentions: s });
416
424
  }
417
425
  deleteComment(e) {
418
426
  this.state.removeComment(e), this.send({ type: "comment:delete", commentId: e });
@@ -421,8 +429,8 @@ class k extends p {
421
429
  this.send({ type: "thread:resolve", threadId: e, resolved: t });
422
430
  }
423
431
  // ── Reactions ──
424
- addReaction(e, t, i) {
425
- this.send({ type: "reaction:add", targetId: e, targetType: t, emoji: i });
432
+ addReaction(e, t, s) {
433
+ this.send({ type: "reaction:add", targetId: e, targetType: t, emoji: s });
426
434
  }
427
435
  removeReaction(e) {
428
436
  this.send({ type: "reaction:remove", reactionId: e });
@@ -455,30 +463,30 @@ class k extends p {
455
463
  this.send({ type: "emoji:drop", emoji: e, position: t });
456
464
  }
457
465
  // ── Drawing ──
458
- drawStroke(e, t, i) {
459
- this.send({ type: "draw:stroke", points: e, color: t, width: i });
466
+ drawStroke(e, t, s) {
467
+ this.send({ type: "draw:stroke", points: e, color: t, width: s });
460
468
  }
461
469
  clearDrawing() {
462
470
  this.send({ type: "draw:clear" });
463
471
  }
464
472
  // ── File Upload ──
465
473
  async uploadFile(e) {
466
- const t = typeof window < "u" ? window.location.origin : "", i = (this.config.endpoint ?? t).replace(/^ws(s?):/, "http$1:"), s = new FormData();
467
- s.append("file", e);
468
- const n = await fetch(`${i}/api/v1/upload`, {
474
+ const t = typeof window < "u" ? window.location.origin : "", s = (this.config.endpoint ?? t).replace(/^ws(s?):/, "http$1:"), i = new FormData();
475
+ i.append("file", e);
476
+ const n = await fetch(`${s}/api/v1/upload`, {
469
477
  method: "POST",
470
478
  headers: {
471
479
  "X-Pulse-Key": this.config.apiKey,
472
480
  "X-Pulse-Token": this.config.token
473
481
  },
474
- body: s
482
+ body: i
475
483
  });
476
484
  if (!n.ok) {
477
485
  const c = await n.json().catch(() => ({ error: "Upload failed" }));
478
486
  throw new Error(c.error ?? "Upload failed");
479
487
  }
480
488
  const o = await n.json();
481
- return o.url && !o.url.startsWith("http") && (o.url = `${i}${o.url}`), o.thumbnailUrl && !o.thumbnailUrl.startsWith("http") && (o.thumbnailUrl = `${i}${o.thumbnailUrl}`), o;
489
+ return o.url && !o.url.startsWith("http") && (o.url = `${s}${o.url}`), o.thumbnailUrl && !o.thumbnailUrl.startsWith("http") && (o.thumbnailUrl = `${s}${o.thumbnailUrl}`), o;
482
490
  }
483
491
  // ── Presence control ──
484
492
  setAppearOffline(e) {
@@ -519,7 +527,7 @@ class k extends p {
519
527
  n.ok && (t = (await n.json()).iceServers);
520
528
  } catch {
521
529
  }
522
- const { P2PManager: i } = await import("./P2PManager-CwR8sbb_.js"), s = new i({
530
+ const { P2PManager: s } = await import("./P2PManager-CwR8sbb_.js"), i = new s({
523
531
  roomId: this.config.room,
524
532
  userId: e.userId,
525
533
  baseUrl: this.state.baseUrl,
@@ -528,15 +536,20 @@ class k extends p {
528
536
  iceServers: t,
529
537
  sendWS: (n) => this.send(n)
530
538
  });
531
- this._p2pInstance = s, await s.initialize();
539
+ this._p2pInstance = i, await i.initialize();
532
540
  for (const n of this.state.presence)
533
- n.user.id !== e.userId && s.onPeerJoined(n.user.id);
534
- return s;
541
+ n.user.id !== e.userId && i.onPeerJoined(n.user.id);
542
+ for (const n of this.signalingQueue)
543
+ this.routeP2PMessage(n);
544
+ return this.signalingQueue = [], i;
545
+ }
546
+ routeP2PMessage(e) {
547
+ this._p2pInstance && (e.type === "signal:offer" ? this._p2pInstance.handleSignalOffer(e.fromUserId, e.sdp) : e.type === "signal:answer" ? this._p2pInstance.handleSignalAnswer(e.fromUserId, e.sdp) : e.type === "signal:ice" ? this._p2pInstance.handleIceCandidate(e.fromUserId, e.candidate) : e.type === "p2p:sync" && this._p2pInstance.handleP2PSync(e.fromUserId, e.update));
535
548
  }
536
549
  }
537
550
  export {
538
- v as Connection,
539
- p as Emitter,
540
- k as PulseClient,
551
+ y as Connection,
552
+ l as Emitter,
553
+ C as PulseClient,
541
554
  I as StateManager
542
555
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gamention/pulse-core",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "Core client SDK for Pulse — WebSocket connection, state management, and API for real-time collaboration",
5
5
  "type": "module",
6
6
  "main": "./dist/pulse-core.cjs",