@gamention/pulse-core 0.2.0 → 0.3.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.
@@ -1,10 +1,10 @@
1
- var p = Object.defineProperty;
2
- var f = (h, n, t) => n in h ? p(h, n, { enumerable: !0, configurable: !0, writable: !0, value: t }) : h[n] = t;
3
- var i = (h, n, t) => f(h, typeof n != "symbol" ? n + "" : n, t);
4
- import { DEFAULT_ENDPOINT as m, RECONNECT_BASE_DELAY_MS as _, RECONNECT_MAX_DELAY_MS as w, DEFAULT_ENV_CONFIG as c, CURSOR_THROTTLE_MS as l, PRESENCE_HEARTBEAT_MS as v } from "@gamention/pulse-shared";
5
- class d {
1
+ var u = Object.defineProperty;
2
+ var f = (h, n, t) => n in h ? u(h, n, { enumerable: !0, configurable: !0, writable: !0, value: t }) : h[n] = t;
3
+ var r = (h, n, t) => f(h, typeof n != "symbol" ? n + "" : n, t);
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 {
6
6
  constructor() {
7
- i(this, "handlers", /* @__PURE__ */ new Map());
7
+ r(this, "handlers", /* @__PURE__ */ new Map());
8
8
  }
9
9
  on(n, t) {
10
10
  this.handlers.has(n) || this.handlers.set(n, /* @__PURE__ */ new Set());
@@ -17,22 +17,22 @@ class d {
17
17
  }
18
18
  emit(n, t) {
19
19
  var e;
20
- (e = this.handlers.get(n)) == null || e.forEach((s) => s(t));
20
+ (e = this.handlers.get(n)) == null || e.forEach((i) => i(t));
21
21
  }
22
22
  removeAll() {
23
23
  this.handlers.clear();
24
24
  }
25
25
  }
26
- class y extends d {
26
+ class v extends p {
27
27
  constructor(t) {
28
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
- i(this, "permanentlyClosed", !1);
35
- this.endpoint = t ?? m;
29
+ r(this, "ws", null);
30
+ r(this, "endpoint");
31
+ r(this, "reconnectAttempt", 0);
32
+ r(this, "reconnectTimer", null);
33
+ r(this, "_state", "disconnected");
34
+ r(this, "permanentlyClosed", !1);
35
+ this.endpoint = t ?? _;
36
36
  }
37
37
  get state() {
38
38
  return this._state;
@@ -67,7 +67,7 @@ class y extends d {
67
67
  scheduleReconnect() {
68
68
  if (this.permanentlyClosed) return;
69
69
  const t = Math.min(
70
- _ * 2 ** this.reconnectAttempt,
70
+ m * 2 ** this.reconnectAttempt,
71
71
  w
72
72
  );
73
73
  this.reconnectAttempt++, this.reconnectTimer = setTimeout(() => {
@@ -75,35 +75,39 @@ class y extends d {
75
75
  }, t);
76
76
  }
77
77
  }
78
- class I extends d {
78
+ class I extends p {
79
79
  constructor() {
80
80
  super(...arguments);
81
81
  /** HTTP base URL for resolving relative attachment paths (e.g. "http://localhost:4000"). */
82
- i(this, "baseUrl", "");
83
- i(this, "_user", null);
84
- i(this, "_config", { ...c });
85
- i(this, "_users", /* @__PURE__ */ new Map());
86
- i(this, "_presence", /* @__PURE__ */ new Map());
87
- i(this, "_threads", /* @__PURE__ */ new Map());
88
- i(this, "_reactions", /* @__PURE__ */ new Map());
89
- i(this, "_notifications", []);
90
- i(this, "_activityLogs", []);
91
- i(this, "_typing", /* @__PURE__ */ new Map());
92
- i(this, "_viewports", /* @__PURE__ */ new Map());
93
- i(this, "_selections", /* @__PURE__ */ new Map());
82
+ r(this, "baseUrl", "");
83
+ r(this, "_user", null);
84
+ r(this, "_config", { ...d });
85
+ r(this, "_p2pConfig", null);
86
+ r(this, "_users", /* @__PURE__ */ new Map());
87
+ r(this, "_presence", /* @__PURE__ */ new Map());
88
+ r(this, "_threads", /* @__PURE__ */ new Map());
89
+ r(this, "_reactions", /* @__PURE__ */ new Map());
90
+ r(this, "_notifications", []);
91
+ r(this, "_activityLogs", []);
92
+ r(this, "_typing", /* @__PURE__ */ new Map());
93
+ r(this, "_viewports", /* @__PURE__ */ new Map());
94
+ r(this, "_selections", /* @__PURE__ */ new Map());
94
95
  }
95
96
  get user() {
96
97
  return this._user;
97
98
  }
99
+ get p2pConfig() {
100
+ return this._p2pConfig;
101
+ }
98
102
  get config() {
99
103
  return this._config;
100
104
  }
101
105
  /** Optimistically remove a comment from local state (before server round-trip). */
102
106
  removeComment(t) {
103
- for (const [e, s] of this._threads) {
104
- const r = s.comments.findIndex((o) => o.id === t);
105
- if (r !== -1) {
106
- s.comments.splice(r, 1), s.comments.length === 0 && this._threads.delete(e), this.emit("threads", this.threads);
107
+ for (const [e, i] of this._threads) {
108
+ const s = i.comments.findIndex((a) => a.id === t);
109
+ if (s !== -1) {
110
+ i.comments.splice(s, 1), i.comments.length === 0 && this._threads.delete(e), this.emit("threads", this.threads);
107
111
  return;
108
112
  }
109
113
  }
@@ -122,7 +126,7 @@ class I extends d {
122
126
  }
123
127
  /** Optimistically mark a single notification as read. */
124
128
  markNotificationRead(t) {
125
- const e = this._notifications.find((s) => s.id === t);
129
+ const e = this._notifications.find((i) => i.id === t);
126
130
  e && !e.read && (e.read = !0, this.emit("notifications", this._notifications));
127
131
  }
128
132
  /** Optimistically mark all notifications as read. */
@@ -148,10 +152,10 @@ class I extends d {
148
152
  getTypingUsers(t) {
149
153
  const e = this._typing.get(t);
150
154
  if (!e) return [];
151
- const s = Date.now(), r = [];
152
- for (const [o, a] of e)
153
- s - a < 3e3 && r.push(o);
154
- return r;
155
+ const i = Date.now(), s = [];
156
+ for (const [a, o] of e)
157
+ i - o < 3e3 && s.push(a);
158
+ return s;
155
159
  }
156
160
  get viewports() {
157
161
  return this._viewports;
@@ -185,7 +189,7 @@ class I extends d {
185
189
  handleMessage(t) {
186
190
  switch (t.type) {
187
191
  case "auth:ok":
188
- this._config = t.config ?? { ...c }, this._user = t.user, this._users.clear();
192
+ this._config = t.config ?? { ...d }, this._p2pConfig = t.p2pConfig ?? null, this._user = t.user, this._users.clear();
189
193
  for (const e of t.users) this._users.set(e.id, e);
190
194
  this._presence.clear();
191
195
  for (const e of t.presence)
@@ -194,8 +198,8 @@ class I extends d {
194
198
  for (const e of t.threads) this._threads.set(e.id, this.resolveThread(e));
195
199
  this._notifications = t.notifications, this._reactions.clear();
196
200
  for (const e of t.reactions) {
197
- const s = this._reactions.get(e.targetId) ?? [];
198
- s.push(e), this._reactions.set(e.targetId, s);
201
+ const i = this._reactions.get(e.targetId) ?? [];
202
+ i.push(e), this._reactions.set(e.targetId, i);
199
203
  }
200
204
  this._activityLogs = [...t.activityLogs], this.emit("auth", t.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);
201
205
  break;
@@ -230,14 +234,14 @@ class I extends d {
230
234
  case "comment:edited": {
231
235
  const e = this._threads.get(t.threadId);
232
236
  if (e) {
233
- const s = e.comments.findIndex((r) => r.id === t.comment.id);
234
- s !== -1 && (e.comments[s] = this.resolveComment(t.comment)), this.emit("threads", this.threads);
237
+ const i = e.comments.findIndex((s) => s.id === t.comment.id);
238
+ i !== -1 && (e.comments[i] = this.resolveComment(t.comment)), this.emit("threads", this.threads);
235
239
  }
236
240
  break;
237
241
  }
238
242
  case "comment:deleted": {
239
243
  const e = this._threads.get(t.threadId);
240
- e && (e.comments = e.comments.filter((s) => s.id !== t.commentId), e.comments.length === 0 && this._threads.delete(t.threadId), this.emit("threads", this.threads));
244
+ e && (e.comments = e.comments.filter((i) => i.id !== t.commentId), e.comments.length === 0 && this._threads.delete(t.threadId), this.emit("threads", this.threads));
241
245
  break;
242
246
  }
243
247
  case "thread:resolved": {
@@ -259,10 +263,10 @@ class I extends d {
259
263
  case "reaction:removed": {
260
264
  const e = this._reactions.get(t.targetId);
261
265
  if (e) {
262
- const s = e.filter((r) => r.id !== t.reactionId);
263
- this._reactions.set(t.targetId, s), this.emit("reactions", {
266
+ const i = e.filter((s) => s.id !== t.reactionId);
267
+ this._reactions.set(t.targetId, i), this.emit("reactions", {
264
268
  targetId: t.targetId,
265
- reactions: s
269
+ reactions: i
266
270
  });
267
271
  }
268
272
  break;
@@ -318,26 +322,30 @@ class I extends d {
318
322
  }
319
323
  }
320
324
  reset() {
321
- this._user = null, this._config = { ...c }, 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();
325
+ 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();
322
326
  }
323
327
  }
324
- class T extends d {
328
+ class k extends p {
325
329
  constructor(t) {
326
- var s;
330
+ var i;
327
331
  super();
328
- i(this, "state");
329
- i(this, "connection");
330
- i(this, "config");
331
- i(this, "heartbeatTimer", null);
332
- i(this, "lastCursorSend", 0);
333
- i(this, "pendingCursor", null);
334
- i(this, "cursorTimer", null);
332
+ r(this, "state");
333
+ r(this, "connection");
334
+ r(this, "config");
335
+ r(this, "heartbeatTimer", null);
336
+ r(this, "lastCursorSend", 0);
337
+ r(this, "pendingCursor", null);
338
+ r(this, "cursorTimer", null);
339
+ r(this, "_p2p", null);
340
+ r(this, "_p2pInstance", null);
341
+ r(this, "p2pEnabled");
342
+ r(this, "iceServers");
335
343
  this.config = t, this.state = new I(), this.state.baseUrl = (t.endpoint ?? "").replace(/^ws(s?):/, "http$1:").replace(/\/$/, "");
336
- const e = ((s = t.endpoint) == null ? void 0 : s.replace(/^http/, "ws")) ?? void 0;
337
- this.connection = new y(e), this.connection.on("message", (r) => {
338
- this.state.handleMessage(r), this.emit(r.type, r), r.type === "auth:error" && this.connection.permanentDisconnect();
339
- }), this.connection.on("state", (r) => {
340
- this.emit("connection", r), r === "connected" ? (this.authenticate(), this.startHeartbeat()) : r === "disconnected" && this.stopHeartbeat();
344
+ const e = ((i = t.endpoint) == null ? void 0 : i.replace(/^http/, "ws")) ?? void 0;
345
+ this.connection = new v(e), this.p2pEnabled = t.p2p ?? !1, this.iceServers = t.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();
341
349
  });
342
350
  }
343
351
  /** Current WebSocket connection state. */
@@ -351,7 +359,8 @@ class T extends d {
351
359
  this.stopHeartbeat(), this.cursorTimer && (clearTimeout(this.cursorTimer), this.cursorTimer = null), this.connection.disconnect(), this.state.reset();
352
360
  }
353
361
  destroy() {
354
- this.disconnect(), this.removeAll(), this.state.removeAll(), this.connection.removeAll();
362
+ var t;
363
+ (t = this._p2pInstance) == null || t.destroy(), this._p2pInstance = null, this._p2p = null, this.disconnect(), this.removeAll(), this.state.removeAll(), this.connection.removeAll();
355
364
  }
356
365
  authenticate() {
357
366
  this.send({
@@ -381,29 +390,29 @@ class T extends d {
381
390
  startHeartbeat() {
382
391
  this.heartbeatTimer || (this.heartbeatTimer = setInterval(() => {
383
392
  this.send({ type: "presence:update", status: "online" });
384
- }, v));
393
+ }, y));
385
394
  }
386
395
  stopHeartbeat() {
387
396
  this.heartbeatTimer && (clearInterval(this.heartbeatTimer), this.heartbeatTimer = null);
388
397
  }
389
398
  // ── Threads & Comments ──
390
399
  createThread(t, e = {}) {
391
- const s = crypto.randomUUID();
400
+ const i = crypto.randomUUID();
392
401
  return this.send({
393
402
  type: "thread:create",
394
- id: s,
403
+ id: i,
395
404
  body: t,
396
405
  mentions: e.mentions ?? [],
397
406
  position: e.position ?? null,
398
407
  attachmentIds: e.attachmentIds
399
- }), s;
408
+ }), i;
400
409
  }
401
- reply(t, e, s = [], r) {
402
- const o = crypto.randomUUID();
403
- return this.send({ type: "comment:create", threadId: t, id: o, body: e, mentions: s, attachmentIds: r }), o;
410
+ reply(t, e, i = [], s) {
411
+ const a = crypto.randomUUID();
412
+ return this.send({ type: "comment:create", threadId: t, id: a, body: e, mentions: i, attachmentIds: s }), a;
404
413
  }
405
- editComment(t, e, s = []) {
406
- this.send({ type: "comment:edit", commentId: t, body: e, mentions: s });
414
+ editComment(t, e, i = []) {
415
+ this.send({ type: "comment:edit", commentId: t, body: e, mentions: i });
407
416
  }
408
417
  deleteComment(t) {
409
418
  this.state.removeComment(t), this.send({ type: "comment:delete", commentId: t });
@@ -412,8 +421,8 @@ class T extends d {
412
421
  this.send({ type: "thread:resolve", threadId: t, resolved: e });
413
422
  }
414
423
  // ── Reactions ──
415
- addReaction(t, e, s) {
416
- this.send({ type: "reaction:add", targetId: t, targetType: e, emoji: s });
424
+ addReaction(t, e, i) {
425
+ this.send({ type: "reaction:add", targetId: t, targetType: e, emoji: i });
417
426
  }
418
427
  removeReaction(t) {
419
428
  this.send({ type: "reaction:remove", reactionId: t });
@@ -446,39 +455,85 @@ class T extends d {
446
455
  this.send({ type: "emoji:drop", emoji: t, position: e });
447
456
  }
448
457
  // ── Drawing ──
449
- drawStroke(t, e, s) {
450
- this.send({ type: "draw:stroke", points: t, color: e, width: s });
458
+ drawStroke(t, e, i) {
459
+ this.send({ type: "draw:stroke", points: t, color: e, width: i });
451
460
  }
452
461
  clearDrawing() {
453
462
  this.send({ type: "draw:clear" });
454
463
  }
455
464
  // ── File Upload ──
456
465
  async uploadFile(t) {
457
- const e = typeof window < "u" ? window.location.origin : "", s = (this.config.endpoint ?? e).replace(/^ws(s?):/, "http$1:"), r = new FormData();
458
- r.append("file", t);
459
- const o = await fetch(`${s}/api/v1/upload`, {
466
+ const e = typeof window < "u" ? window.location.origin : "", i = (this.config.endpoint ?? e).replace(/^ws(s?):/, "http$1:"), s = new FormData();
467
+ s.append("file", t);
468
+ const a = await fetch(`${i}/api/v1/upload`, {
460
469
  method: "POST",
461
470
  headers: {
462
471
  "X-Pulse-Key": this.config.apiKey,
463
472
  "X-Pulse-Token": this.config.token
464
473
  },
465
- body: r
474
+ body: s
466
475
  });
467
- if (!o.ok) {
468
- const u = await o.json().catch(() => ({ error: "Upload failed" }));
469
- throw new Error(u.error ?? "Upload failed");
476
+ if (!a.ok) {
477
+ const c = await a.json().catch(() => ({ error: "Upload failed" }));
478
+ throw new Error(c.error ?? "Upload failed");
470
479
  }
471
- const a = await o.json();
472
- return a.url && !a.url.startsWith("http") && (a.url = `${s}${a.url}`), a.thumbnailUrl && !a.thumbnailUrl.startsWith("http") && (a.thumbnailUrl = `${s}${a.thumbnailUrl}`), a;
480
+ const o = await a.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;
473
482
  }
474
483
  // ── Presence control ──
475
484
  setAppearOffline(t) {
476
485
  t ? (this.stopHeartbeat(), this.send({ type: "presence:update", status: "idle" })) : (this.startHeartbeat(), this.send({ type: "presence:update", status: "online" }));
477
486
  }
487
+ // ── P2P ──
488
+ /** Lazy-loaded P2P manager. Returns a promise that resolves to the P2PManager instance. */
489
+ get p2p() {
490
+ return this.p2pEnabled ? (this._p2p || (this._p2p = this.initP2P()), this._p2p) : Promise.reject(
491
+ new Error("P2P is not enabled. Pass { p2p: true } in PulseConfig.")
492
+ );
493
+ }
494
+ async initP2P() {
495
+ const t = await new Promise((a, o) => {
496
+ if (this.state.user && this.state.p2pConfig) {
497
+ a({
498
+ userId: this.state.user.id,
499
+ p2pConfig: this.state.p2pConfig
500
+ });
501
+ return;
502
+ }
503
+ const c = this.state.on("auth", () => {
504
+ if (c(), !this.state.p2pConfig) {
505
+ o(new Error("P2P is not enabled for this environment."));
506
+ return;
507
+ }
508
+ a({
509
+ userId: this.state.user.id,
510
+ p2pConfig: this.state.p2pConfig
511
+ });
512
+ });
513
+ });
514
+ let e = this.iceServers;
515
+ try {
516
+ const a = await fetch(`${this.state.baseUrl}/api/v1/turn-credentials`, {
517
+ headers: { Authorization: `Bearer ${this.config.token}` }
518
+ });
519
+ a.ok && (e = (await a.json()).iceServers);
520
+ } catch {
521
+ }
522
+ const { P2PManager: i } = await import("./P2PManager-B21XQVk_.js"), s = new i({
523
+ roomId: this.config.room,
524
+ userId: t.userId,
525
+ baseUrl: this.state.baseUrl,
526
+ authToken: this.config.token,
527
+ p2pConfig: t.p2pConfig,
528
+ iceServers: e,
529
+ sendWS: (a) => this.send(a)
530
+ });
531
+ return this._p2pInstance = s, await s.initialize(), s;
532
+ }
478
533
  }
479
534
  export {
480
- y as Connection,
481
- d as Emitter,
482
- T as PulseClient,
535
+ v as Connection,
536
+ p as Emitter,
537
+ k as PulseClient,
483
538
  I as StateManager
484
539
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gamention/pulse-core",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
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",
@@ -16,6 +16,11 @@
16
16
  "files": [
17
17
  "dist"
18
18
  ],
19
+ "scripts": {
20
+ "build": "vite build",
21
+ "dev": "vite build --watch",
22
+ "clean": "rm -rf dist"
23
+ },
19
24
  "keywords": [
20
25
  "pulse",
21
26
  "collaboration",
@@ -36,17 +41,14 @@
36
41
  "access": "public"
37
42
  },
38
43
  "peerDependencies": {
39
- "@gamention/pulse-shared": "0.2.0"
44
+ "@gamention/pulse-shared": "^0.3.0"
45
+ },
46
+ "dependencies": {
47
+ "yjs": "^13.6.30"
40
48
  },
41
- "dependencies": {},
42
49
  "devDependencies": {
43
50
  "typescript": "^5.7.0",
44
51
  "vite": "^6.0.0",
45
52
  "vite-plugin-dts": "^4.3.0"
46
- },
47
- "scripts": {
48
- "build": "vite build",
49
- "dev": "vite build --watch",
50
- "clean": "rm -rf dist"
51
53
  }
52
- }
54
+ }