@gamention/pulse-core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,120 @@
1
+ import { ActivityLog } from '@gamention/pulse-shared';
2
+ import { ClientMessage } from '@gamention/pulse-shared';
3
+ import { CursorPosition } from '@gamention/pulse-shared';
4
+ import { Notification as Notification_2 } from '@gamention/pulse-shared';
5
+ import { PinPosition } from '@gamention/pulse-shared';
6
+ import { PresenceUser } from '@gamention/pulse-shared';
7
+ import { PulseConfig } from '@gamention/pulse-shared';
8
+ import { PulseUser } from '@gamention/pulse-shared';
9
+ import { Reaction } from '@gamention/pulse-shared';
10
+ import { SelectionRange } from '@gamention/pulse-shared';
11
+ import { ServerMessage } from '@gamention/pulse-shared';
12
+ import { Thread } from '@gamention/pulse-shared';
13
+ import { ViewportInfo } from '@gamention/pulse-shared';
14
+
15
+ export declare class Connection extends Emitter {
16
+ private ws;
17
+ private endpoint;
18
+ private reconnectAttempt;
19
+ private reconnectTimer;
20
+ private _state;
21
+ get state(): ConnectionState;
22
+ constructor(endpoint?: string);
23
+ connect(): void;
24
+ disconnect(): void;
25
+ send(message: ClientMessage): void;
26
+ private scheduleReconnect;
27
+ }
28
+
29
+ export declare type ConnectionState = "disconnected" | "connecting" | "connected";
30
+
31
+ export declare class Emitter {
32
+ private handlers;
33
+ on<T = unknown>(event: string, handler: Handler<T>): () => void;
34
+ off<T = unknown>(event: string, handler: Handler<T>): void;
35
+ emit<T = unknown>(event: string, data: T): void;
36
+ removeAll(): void;
37
+ }
38
+
39
+ declare type Handler<T = unknown> = (data: T) => void;
40
+
41
+ export declare class PulseClient extends Emitter {
42
+ readonly state: StateManager;
43
+ private connection;
44
+ private config;
45
+ private heartbeatTimer;
46
+ private lastCursorSend;
47
+ private pendingCursor;
48
+ private cursorTimer;
49
+ /** Current WebSocket connection state. */
50
+ get connectionState(): ConnectionState;
51
+ constructor(config: PulseConfig);
52
+ connect(): void;
53
+ disconnect(): void;
54
+ private authenticate;
55
+ send(message: ClientMessage): void;
56
+ moveCursor(position: CursorPosition): void;
57
+ private flushCursor;
58
+ updatePresence(status: "online" | "idle"): void;
59
+ private startHeartbeat;
60
+ private stopHeartbeat;
61
+ createThread(body: string, options?: {
62
+ position?: PinPosition;
63
+ mentions?: string[];
64
+ }): string;
65
+ reply(threadId: string, body: string, mentions?: string[]): string;
66
+ editComment(commentId: string, body: string, mentions?: string[]): void;
67
+ deleteComment(commentId: string): void;
68
+ resolveThread(threadId: string, resolved?: boolean): void;
69
+ addReaction(targetId: string, targetType: "comment" | "thread", emoji: string): void;
70
+ removeReaction(reactionId: string): void;
71
+ markRead(notificationId: string): void;
72
+ markAllRead(): void;
73
+ performClick(position: CursorPosition): void;
74
+ sendTyping(threadId: string): void;
75
+ updateViewport(info: ViewportInfo): void;
76
+ updateSelection(selection: SelectionRange | null): void;
77
+ dropEmoji(emoji: string, position: CursorPosition): void;
78
+ drawStroke(points: {
79
+ x: number;
80
+ y: number;
81
+ }[], color: string, width: number): void;
82
+ clearDrawing(): void;
83
+ uploadFile(file: File): Promise<{
84
+ id: string;
85
+ type: string;
86
+ filename: string;
87
+ url: string;
88
+ thumbnailUrl?: string;
89
+ }>;
90
+ setAppearOffline(offline: boolean): void;
91
+ }
92
+
93
+ export declare class StateManager extends Emitter {
94
+ private _user;
95
+ private _users;
96
+ private _presence;
97
+ private _threads;
98
+ private _reactions;
99
+ private _notifications;
100
+ private _activityLogs;
101
+ private _typing;
102
+ private _viewports;
103
+ private _selections;
104
+ get user(): PulseUser | null;
105
+ get presence(): PresenceUser[];
106
+ get threads(): Thread[];
107
+ get notifications(): Notification_2[];
108
+ get unreadCount(): number;
109
+ get activityLogs(): ActivityLog[];
110
+ getUser(userId: string): PulseUser | undefined;
111
+ getReactions(targetId: string): Reaction[];
112
+ getTypingUsers(threadId: string): string[];
113
+ get viewports(): Map<string, ViewportInfo>;
114
+ getViewport(userId: string): ViewportInfo | undefined;
115
+ get selections(): Map<string, SelectionRange | null>;
116
+ handleMessage(msg: ServerMessage): void;
117
+ reset(): void;
118
+ }
119
+
120
+ export { }
@@ -0,0 +1 @@
1
+ "use strict";var u=Object.defineProperty;var _=(a,n,e)=>n in a?u(a,n,{enumerable:!0,configurable:!0,writable:!0,value:e}):a[n]=e;var i=(a,n,e)=>_(a,typeof n!="symbol"?n+"":n,e);Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const f="ws://localhost:4567",h=50,w=3e4,y=1e3,m=3e4;class o{constructor(){i(this,"handlers",new Map)}on(n,e){this.handlers.has(n)||this.handlers.set(n,new Set);const t=this.handlers.get(n);return t.add(e),()=>t.delete(e)}off(n,e){var t;(t=this.handlers.get(n))==null||t.delete(e)}emit(n,e){var t;(t=this.handlers.get(n))==null||t.forEach(s=>s(e))}removeAll(){this.handlers.clear()}}class d extends o{constructor(e){super();i(this,"ws",null);i(this,"endpoint");i(this,"reconnectAttempt",0);i(this,"reconnectTimer",null);i(this,"_state","disconnected");this.endpoint=e??f}get state(){return this._state}connect(){this.ws||(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=>{const t=JSON.parse(e.data);this.emit("message",t)}),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))}scheduleReconnect(){const e=Math.min(y*2**this.reconnectAttempt,m);this.reconnectAttempt++,this.reconnectTimer=setTimeout(()=>{this.reconnectTimer=null,this.connect()},e)}}class p extends o{constructor(){super(...arguments);i(this,"_user",null);i(this,"_users",new Map);i(this,"_presence",new Map);i(this,"_threads",new Map);i(this,"_reactions",new Map);i(this,"_notifications",[]);i(this,"_activityLogs",[]);i(this,"_typing",new Map);i(this,"_viewports",new Map);i(this,"_selections",new Map)}get user(){return this._user}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}get activityLogs(){return this._activityLogs}getUser(e){return this._users.get(e)}getReactions(e){return this._reactions.get(e)??[]}getTypingUsers(e){const t=this._typing.get(e);if(!t)return[];const s=Date.now(),r=[];for(const[c,l]of t)s-l<3e3&&r.push(c);return r}get viewports(){return this._viewports}getViewport(e){return this._viewports.get(e)}get selections(){return this._selections}handleMessage(e){switch(e.type){case"auth:ok":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,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,e.thread),this.emit("threads",this.threads);break;case"comment:created":{const t=this._threads.get(e.threadId);t&&(t.comments.push(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(r=>r.id===e.comment.id);s!==-1&&(t.comments[s]=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(r=>r.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"error":this.emit("error",e);break}}reset(){this._user=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 v extends o{constructor(e){var s;super();i(this,"state");i(this,"connection");i(this,"config");i(this,"heartbeatTimer",null);i(this,"lastCursorSend",0);i(this,"pendingCursor",null);i(this,"cursorTimer",null);this.config=e,this.state=new p;const t=((s=e.endpoint)==null?void 0:s.replace(/^http/,"ws"))??void 0;this.connection=new d(t),this.connection.on("message",r=>{this.state.handleMessage(r),this.emit(r.type,r)}),this.connection.on("state",r=>{this.emit("connection",r),r==="connected"?(this.authenticate(),this.startHeartbeat()):r==="disconnected"&&this.stopHeartbeat()})}get connectionState(){return this.connection.state}connect(){this.connection.connect()}disconnect(){this.stopHeartbeat(),this.connection.disconnect(),this.state.reset()}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>=h?this.flushCursor():this.cursorTimer||(this.cursorTimer=setTimeout(()=>{this.cursorTimer=null,this.flushCursor()},h))}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=setInterval(()=>{this.send({type:"presence:update",status:"online"})},w)}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}),s}reply(e,t,s=[]){const r=crypto.randomUUID();return this.send({type:"comment:create",threadId:e,id:r,body:t,mentions:s}),r}editComment(e,t,s=[]){this.send({type:"comment:edit",commentId:e,body:t,mentions:s})}deleteComment(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.send({type:"notification:read",notificationId:e})}markAllRead(){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=this.config.endpoint??window.location.origin,s=new FormData;s.append("file",e);const r=await fetch(`${t}/api/v1/upload`,{method:"POST",headers:{Authorization:`Bearer ${this.config.apiKey}`},body:s});if(!r.ok){const c=await r.json().catch(()=>({error:"Upload failed"}));throw new Error(c.error??"Upload failed")}return r.json()}setAppearOffline(e){e?(this.stopHeartbeat(),this.send({type:"presence:update",status:"idle"})):(this.startHeartbeat(),this.send({type:"presence:update",status:"online"}))}}exports.Connection=d;exports.Emitter=o;exports.PulseClient=v;exports.StateManager=p;
@@ -0,0 +1,415 @@
1
+ var d = Object.defineProperty;
2
+ var p = (a, n, e) => n in a ? d(a, n, { enumerable: !0, configurable: !0, writable: !0, value: e }) : a[n] = e;
3
+ var i = (a, n, e) => p(a, typeof n != "symbol" ? n + "" : n, e);
4
+ const l = "ws://localhost:4567";
5
+ class c {
6
+ constructor() {
7
+ i(this, "handlers", /* @__PURE__ */ new Map());
8
+ }
9
+ on(n, e) {
10
+ this.handlers.has(n) || this.handlers.set(n, /* @__PURE__ */ new Set());
11
+ const t = this.handlers.get(n);
12
+ return t.add(e), () => t.delete(e);
13
+ }
14
+ off(n, e) {
15
+ var t;
16
+ (t = this.handlers.get(n)) == null || t.delete(e);
17
+ }
18
+ emit(n, e) {
19
+ var t;
20
+ (t = this.handlers.get(n)) == null || t.forEach((s) => s(e));
21
+ }
22
+ removeAll() {
23
+ this.handlers.clear();
24
+ }
25
+ }
26
+ class u extends c {
27
+ constructor(e) {
28
+ super();
29
+ i(this, "ws", null);
30
+ i(this, "endpoint");
31
+ i(this, "reconnectAttempt", 0);
32
+ i(this, "reconnectTimer", null);
33
+ i(this, "_state", "disconnected");
34
+ this.endpoint = e ?? l;
35
+ }
36
+ get state() {
37
+ return this._state;
38
+ }
39
+ connect() {
40
+ this.ws || (this._state = "connecting", this.emit("state", this._state), this.ws = new WebSocket(this.endpoint), this.ws.addEventListener("open", () => {
41
+ this._state = "connected", this.reconnectAttempt = 0, this.emit("state", this._state);
42
+ }), this.ws.addEventListener("message", (e) => {
43
+ const t = JSON.parse(e.data);
44
+ this.emit("message", t);
45
+ }), this.ws.addEventListener("close", () => {
46
+ this.ws = null, this._state = "disconnected", this.emit("state", this._state), this.scheduleReconnect();
47
+ }), this.ws.addEventListener("error", () => {
48
+ var e;
49
+ (e = this.ws) == null || e.close();
50
+ }));
51
+ }
52
+ disconnect() {
53
+ var e;
54
+ 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);
55
+ }
56
+ send(e) {
57
+ var t;
58
+ ((t = this.ws) == null ? void 0 : t.readyState) === WebSocket.OPEN && this.ws.send(JSON.stringify(e));
59
+ }
60
+ scheduleReconnect() {
61
+ const e = Math.min(
62
+ 1e3 * 2 ** this.reconnectAttempt,
63
+ 3e4
64
+ );
65
+ this.reconnectAttempt++, this.reconnectTimer = setTimeout(() => {
66
+ this.reconnectTimer = null, this.connect();
67
+ }, e);
68
+ }
69
+ }
70
+ class _ extends c {
71
+ constructor() {
72
+ super(...arguments);
73
+ i(this, "_user", null);
74
+ i(this, "_users", /* @__PURE__ */ new Map());
75
+ i(this, "_presence", /* @__PURE__ */ new Map());
76
+ i(this, "_threads", /* @__PURE__ */ new Map());
77
+ i(this, "_reactions", /* @__PURE__ */ new Map());
78
+ i(this, "_notifications", []);
79
+ i(this, "_activityLogs", []);
80
+ i(this, "_typing", /* @__PURE__ */ new Map());
81
+ i(this, "_viewports", /* @__PURE__ */ new Map());
82
+ i(this, "_selections", /* @__PURE__ */ new Map());
83
+ }
84
+ get user() {
85
+ return this._user;
86
+ }
87
+ get presence() {
88
+ return [...this._presence.values()];
89
+ }
90
+ get threads() {
91
+ return [...this._threads.values()];
92
+ }
93
+ get notifications() {
94
+ return this._notifications;
95
+ }
96
+ get unreadCount() {
97
+ return this._notifications.filter((e) => !e.read).length;
98
+ }
99
+ get activityLogs() {
100
+ return this._activityLogs;
101
+ }
102
+ getUser(e) {
103
+ return this._users.get(e);
104
+ }
105
+ getReactions(e) {
106
+ return this._reactions.get(e) ?? [];
107
+ }
108
+ getTypingUsers(e) {
109
+ const t = this._typing.get(e);
110
+ if (!t) return [];
111
+ const s = Date.now(), r = [];
112
+ for (const [o, h] of t)
113
+ s - h < 3e3 && r.push(o);
114
+ return r;
115
+ }
116
+ get viewports() {
117
+ return this._viewports;
118
+ }
119
+ getViewport(e) {
120
+ return this._viewports.get(e);
121
+ }
122
+ get selections() {
123
+ return this._selections;
124
+ }
125
+ handleMessage(e) {
126
+ switch (e.type) {
127
+ case "auth:ok":
128
+ this._user = e.user, this._users.clear();
129
+ for (const t of e.users) this._users.set(t.id, t);
130
+ this._presence.clear();
131
+ for (const t of e.presence)
132
+ this._presence.set(t.user.id, t), this._users.set(t.user.id, t.user);
133
+ this._users.set(e.user.id, e.user), this._threads.clear();
134
+ for (const t of e.threads) this._threads.set(t.id, t);
135
+ this._notifications = e.notifications, this._reactions.clear();
136
+ for (const t of e.reactions) {
137
+ const s = this._reactions.get(t.targetId) ?? [];
138
+ s.push(t), this._reactions.set(t.targetId, s);
139
+ }
140
+ 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);
141
+ break;
142
+ case "presence:join":
143
+ this._presence.set(e.user.user.id, e.user), this._users.set(e.user.user.id, e.user.user), this.emit("presence", this.presence);
144
+ break;
145
+ case "presence:leave":
146
+ this._presence.delete(e.userId), this._viewports.delete(e.userId), this._selections.delete(e.userId);
147
+ for (const t of this._typing.values())
148
+ t.delete(e.userId);
149
+ this.emit("presence", this.presence);
150
+ break;
151
+ case "presence:update": {
152
+ const t = this._presence.get(e.userId);
153
+ t && (t.status = e.status, this.emit("presence", this.presence));
154
+ break;
155
+ }
156
+ case "cursor:move":
157
+ this.emit("cursor", { userId: e.userId, position: e.position });
158
+ break;
159
+ case "click:perform":
160
+ this.emit("click", { userId: e.userId, position: e.position });
161
+ break;
162
+ case "thread:created":
163
+ this._threads.set(e.thread.id, e.thread), this.emit("threads", this.threads);
164
+ break;
165
+ case "comment:created": {
166
+ const t = this._threads.get(e.threadId);
167
+ t && (t.comments.push(e.comment), t.updatedAt = e.comment.createdAt, this.emit("threads", this.threads));
168
+ break;
169
+ }
170
+ case "comment:edited": {
171
+ const t = this._threads.get(e.threadId);
172
+ if (t) {
173
+ const s = t.comments.findIndex((r) => r.id === e.comment.id);
174
+ s !== -1 && (t.comments[s] = e.comment), this.emit("threads", this.threads);
175
+ }
176
+ break;
177
+ }
178
+ case "comment:deleted": {
179
+ const t = this._threads.get(e.threadId);
180
+ 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));
181
+ break;
182
+ }
183
+ case "thread:resolved": {
184
+ const t = this._threads.get(e.threadId);
185
+ t && (t.resolved = e.resolved, this.emit("threads", this.threads));
186
+ break;
187
+ }
188
+ case "thread:deleted":
189
+ this._threads.delete(e.threadId), this.emit("threads", this.threads);
190
+ break;
191
+ case "reaction:added": {
192
+ const t = this._reactions.get(e.reaction.targetId) ?? [];
193
+ t.push(e.reaction), this._reactions.set(e.reaction.targetId, t), this.emit("reactions", {
194
+ targetId: e.reaction.targetId,
195
+ reactions: t
196
+ });
197
+ break;
198
+ }
199
+ case "reaction:removed": {
200
+ const t = this._reactions.get(e.targetId);
201
+ if (t) {
202
+ const s = t.filter((r) => r.id !== e.reactionId);
203
+ this._reactions.set(e.targetId, s), this.emit("reactions", {
204
+ targetId: e.targetId,
205
+ reactions: s
206
+ });
207
+ }
208
+ break;
209
+ }
210
+ case "notification":
211
+ this._notifications.unshift(e.notification), this.emit("notifications", this._notifications);
212
+ break;
213
+ case "typing:indicator": {
214
+ this._typing.has(e.threadId) || this._typing.set(e.threadId, /* @__PURE__ */ new Map()), this._typing.get(e.threadId).set(e.userId, Date.now()), this.emit("typing", { threadId: e.threadId, userId: e.userId });
215
+ break;
216
+ }
217
+ case "viewport:update": {
218
+ this._viewports.set(e.userId, {
219
+ scrollX: e.scrollX,
220
+ scrollY: e.scrollY,
221
+ viewportWidth: e.viewportWidth,
222
+ viewportHeight: e.viewportHeight,
223
+ pageWidth: e.pageWidth,
224
+ pageHeight: e.pageHeight
225
+ }), this.emit("viewport", { userId: e.userId });
226
+ break;
227
+ }
228
+ case "selection:update":
229
+ this._selections.set(e.userId, e.selection), this.emit("selection", { userId: e.userId, selection: e.selection });
230
+ break;
231
+ case "emoji:drop":
232
+ this.emit("emoji-drop", {
233
+ userId: e.userId,
234
+ emoji: e.emoji,
235
+ position: e.position
236
+ });
237
+ break;
238
+ case "draw:stroke":
239
+ this.emit("draw-stroke", {
240
+ userId: e.userId,
241
+ points: e.points,
242
+ color: e.color,
243
+ width: e.width
244
+ });
245
+ break;
246
+ case "draw:clear":
247
+ this.emit("draw-clear", { userId: e.userId });
248
+ break;
249
+ case "activity:logged":
250
+ this._activityLogs.unshift(e.activityLog), this._activityLogs.length > 100 && (this._activityLogs = this._activityLogs.slice(0, 100)), this.emit("activity-logs", this._activityLogs);
251
+ break;
252
+ case "error":
253
+ this.emit("error", e);
254
+ break;
255
+ }
256
+ }
257
+ reset() {
258
+ this._user = 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();
259
+ }
260
+ }
261
+ class w extends c {
262
+ constructor(e) {
263
+ var s;
264
+ super();
265
+ i(this, "state");
266
+ i(this, "connection");
267
+ i(this, "config");
268
+ i(this, "heartbeatTimer", null);
269
+ i(this, "lastCursorSend", 0);
270
+ i(this, "pendingCursor", null);
271
+ i(this, "cursorTimer", null);
272
+ this.config = e, this.state = new _();
273
+ const t = ((s = e.endpoint) == null ? void 0 : s.replace(/^http/, "ws")) ?? void 0;
274
+ this.connection = new u(t), this.connection.on("message", (r) => {
275
+ this.state.handleMessage(r), this.emit(r.type, r);
276
+ }), this.connection.on("state", (r) => {
277
+ this.emit("connection", r), r === "connected" ? (this.authenticate(), this.startHeartbeat()) : r === "disconnected" && this.stopHeartbeat();
278
+ });
279
+ }
280
+ /** Current WebSocket connection state. */
281
+ get connectionState() {
282
+ return this.connection.state;
283
+ }
284
+ connect() {
285
+ this.connection.connect();
286
+ }
287
+ disconnect() {
288
+ this.stopHeartbeat(), this.connection.disconnect(), this.state.reset();
289
+ }
290
+ authenticate() {
291
+ this.send({
292
+ type: "auth",
293
+ apiKey: this.config.apiKey,
294
+ token: this.config.token,
295
+ room: this.config.room
296
+ });
297
+ }
298
+ send(e) {
299
+ this.connection.send(e);
300
+ }
301
+ // ── Cursors ──
302
+ moveCursor(e) {
303
+ const t = Date.now();
304
+ this.pendingCursor = e, t - this.lastCursorSend >= 50 ? this.flushCursor() : this.cursorTimer || (this.cursorTimer = setTimeout(() => {
305
+ this.cursorTimer = null, this.flushCursor();
306
+ }, 50));
307
+ }
308
+ flushCursor() {
309
+ this.pendingCursor && (this.send({ type: "cursor:move", position: this.pendingCursor }), this.lastCursorSend = Date.now(), this.pendingCursor = null);
310
+ }
311
+ // ── Presence ──
312
+ updatePresence(e) {
313
+ this.send({ type: "presence:update", status: e });
314
+ }
315
+ startHeartbeat() {
316
+ this.heartbeatTimer = setInterval(() => {
317
+ this.send({ type: "presence:update", status: "online" });
318
+ }, 3e4);
319
+ }
320
+ stopHeartbeat() {
321
+ this.heartbeatTimer && (clearInterval(this.heartbeatTimer), this.heartbeatTimer = null);
322
+ }
323
+ // ── Threads & Comments ──
324
+ createThread(e, t = {}) {
325
+ const s = crypto.randomUUID();
326
+ return this.send({
327
+ type: "thread:create",
328
+ id: s,
329
+ body: e,
330
+ mentions: t.mentions ?? [],
331
+ position: t.position ?? null
332
+ }), s;
333
+ }
334
+ reply(e, t, s = []) {
335
+ const r = crypto.randomUUID();
336
+ return this.send({ type: "comment:create", threadId: e, id: r, body: t, mentions: s }), r;
337
+ }
338
+ editComment(e, t, s = []) {
339
+ this.send({ type: "comment:edit", commentId: e, body: t, mentions: s });
340
+ }
341
+ deleteComment(e) {
342
+ this.send({ type: "comment:delete", commentId: e });
343
+ }
344
+ resolveThread(e, t = !0) {
345
+ this.send({ type: "thread:resolve", threadId: e, resolved: t });
346
+ }
347
+ // ── Reactions ──
348
+ addReaction(e, t, s) {
349
+ this.send({ type: "reaction:add", targetId: e, targetType: t, emoji: s });
350
+ }
351
+ removeReaction(e) {
352
+ this.send({ type: "reaction:remove", reactionId: e });
353
+ }
354
+ // ── Notifications ──
355
+ markRead(e) {
356
+ this.send({ type: "notification:read", notificationId: e });
357
+ }
358
+ markAllRead() {
359
+ this.send({ type: "notification:read-all" });
360
+ }
361
+ // ── Clicks ──
362
+ performClick(e) {
363
+ this.send({ type: "click:perform", position: e });
364
+ }
365
+ // ── Typing ──
366
+ sendTyping(e) {
367
+ this.send({ type: "typing:start", threadId: e });
368
+ }
369
+ // ── Viewport ──
370
+ updateViewport(e) {
371
+ this.send({ type: "viewport:update", ...e });
372
+ }
373
+ // ── Selection ──
374
+ updateSelection(e) {
375
+ this.send({ type: "selection:update", selection: e });
376
+ }
377
+ // ── Emoji Drops ──
378
+ dropEmoji(e, t) {
379
+ this.send({ type: "emoji:drop", emoji: e, position: t });
380
+ }
381
+ // ── Drawing ──
382
+ drawStroke(e, t, s) {
383
+ this.send({ type: "draw:stroke", points: e, color: t, width: s });
384
+ }
385
+ clearDrawing() {
386
+ this.send({ type: "draw:clear" });
387
+ }
388
+ // ── File Upload ──
389
+ async uploadFile(e) {
390
+ const t = this.config.endpoint ?? window.location.origin, s = new FormData();
391
+ s.append("file", e);
392
+ const r = await fetch(`${t}/api/v1/upload`, {
393
+ method: "POST",
394
+ headers: {
395
+ Authorization: `Bearer ${this.config.apiKey}`
396
+ },
397
+ body: s
398
+ });
399
+ if (!r.ok) {
400
+ const o = await r.json().catch(() => ({ error: "Upload failed" }));
401
+ throw new Error(o.error ?? "Upload failed");
402
+ }
403
+ return r.json();
404
+ }
405
+ // ── Presence control ──
406
+ setAppearOffline(e) {
407
+ e ? (this.stopHeartbeat(), this.send({ type: "presence:update", status: "idle" })) : (this.startHeartbeat(), this.send({ type: "presence:update", status: "online" }));
408
+ }
409
+ }
410
+ export {
411
+ u as Connection,
412
+ c as Emitter,
413
+ w as PulseClient,
414
+ _ as StateManager
415
+ };
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@gamention/pulse-core",
3
+ "version": "0.1.0",
4
+ "description": "Core client SDK for Pulse — WebSocket connection, state management, and API for real-time collaboration",
5
+ "type": "module",
6
+ "main": "./dist/pulse-core.cjs",
7
+ "module": "./dist/pulse-core.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/pulse-core.js",
12
+ "require": "./dist/pulse-core.cjs",
13
+ "types": "./dist/index.d.ts"
14
+ }
15
+ },
16
+ "files": ["dist"],
17
+ "scripts": {
18
+ "build": "vite build",
19
+ "dev": "vite build --watch",
20
+ "clean": "rm -rf dist"
21
+ },
22
+ "keywords": ["pulse", "collaboration", "realtime", "websocket", "client", "sdk"],
23
+ "author": "Krishna Thorat <krishnathorat007@gmail.com>",
24
+ "license": "MIT",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "https://github.com/gamentionkray/pulse.git",
28
+ "directory": "packages/core"
29
+ },
30
+ "homepage": "https://pulse.hire.rest/docs",
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "dependencies": {
35
+ "@gamention/pulse-shared": "workspace:*"
36
+ },
37
+ "devDependencies": {
38
+ "typescript": "^5.7.0",
39
+ "vite": "^6.0.0",
40
+ "vite-plugin-dts": "^4.3.0"
41
+ }
42
+ }