thinkncollab-shell 0.0.5 → 0.0.7

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,17 +1,16 @@
1
1
  /**
2
- * WebSocket Manager for ThinkNCollab Shell
3
- * Uses thinknsh: prefixed events for terminal-specific communication
2
+ * src/core/websocket.js — WebSocket Manager for ThinkNCollab Shell
4
3
  */
5
4
 
6
- const { io } = require('socket.io-client');
5
+ const { io } = require('socket.io-client');
7
6
  const EventEmitter = require('events');
8
7
 
9
8
  class WebSocketManager extends EventEmitter {
9
+
10
10
  constructor(config = {}) {
11
11
  super();
12
-
13
12
  this.config = {
14
- serverUrl: config.serverUrl || 'https://thinkncollab.com',
13
+ serverUrl: config.serverUrl || 'http://localhost:3001',
15
14
  reconnectAttempts: config.reconnectAttempts || 5,
16
15
  reconnectDelay: config.reconnectDelay || 1000,
17
16
  ...config
@@ -19,12 +18,13 @@ class WebSocketManager extends EventEmitter {
19
18
 
20
19
  this.socket = null;
21
20
  this.connected = false;
22
- this.currentUser = null;
21
+ this.currentUser = null; // always { userId, name, userType }
23
22
  this.roomId = null;
24
23
  this.reconnectCount = 0;
25
24
  this.messageQueue = [];
26
25
  this._authToken = null;
27
26
  this._handlersSetup = false;
27
+ this._serverReady = false;
28
28
  }
29
29
 
30
30
  // ─── Connect ──────────────────────────────────────────────────────────────
@@ -32,40 +32,34 @@ class WebSocketManager extends EventEmitter {
32
32
  connect(authToken = null) {
33
33
  return new Promise((resolve, reject) => {
34
34
  try {
35
- this._authToken = authToken;
35
+ if (authToken) this._authToken = authToken;
36
36
 
37
37
  this.socket = io(this.config.serverUrl + '/thinknsh', {
38
38
  transports: ['websocket', 'polling'],
39
39
  reconnection: true,
40
40
  reconnectionAttempts: this.config.reconnectAttempts,
41
41
  reconnectionDelay: this.config.reconnectDelay,
42
- auth: authToken ? { token: authToken } : {},
42
+ auth: this._authToken ? { token: this._authToken } : {},
43
43
  });
44
44
 
45
- const timeout = setTimeout(() => reject(new Error('Connection timeout')), 10000);
45
+ const timeout = setTimeout(() => {
46
+ reject(new Error('Connection timeout'));
47
+ }, 10000);
46
48
 
47
49
  this.socket.on('connect', () => {
48
50
  clearTimeout(timeout);
49
51
  this.connected = true;
50
52
  this.reconnectCount = 0;
51
53
 
54
+ // Announce user immediately on connect if we have one
52
55
  if (this.currentUser) {
53
- console.log('🖥️ Emitting thinknsh:connected for', this.currentUser.userId);
54
- this.socket.emit('thinknsh:connected', {
55
- userId: this.currentUser.userId,
56
- userName: this.currentUser.name,
57
- shellToken: authToken,
58
- });
56
+ this._announceUser();
59
57
  }
60
58
 
61
59
  this.processPendingMessages();
62
60
  this.emit('connected', { socketId: this.socket.id });
63
61
  resolve({ success: true, socketId: this.socket.id });
64
62
  });
65
- // One-time listener for confirmation
66
- // this.ws.once('notified', (data) => {
67
- // console.log(chalk.green(`✅ Notification sent to ${data.sentTo} member(s) in "${data.roomName}"`));
68
- // });
69
63
 
70
64
  this.socket.on('connect_error', (error) => {
71
65
  clearTimeout(timeout);
@@ -81,26 +75,14 @@ class WebSocketManager extends EventEmitter {
81
75
  this.emit('disconnected', { reason });
82
76
  });
83
77
 
84
- this.socket.on('reconnect', (attemptNumber) => {
78
+ this.socket.on('reconnect', () => {
85
79
  this.connected = true;
86
- this.emit('reconnected', { attemptNumber });
87
-
88
- // Re-announce user after reconnect
89
- if (this.currentUser) {
90
- this.socket.emit('thinknsh:connected', {
91
- userId: this.currentUser.userId,
92
- userName: this.currentUser.name,
93
- shellToken: this._authToken,
94
- });
95
- }
80
+ this.emit('reconnected', {});
96
81
 
97
- // Rejoin room after reconnect
98
- if (this.roomId && this.currentUser) {
99
- this._emitJoin(this.roomId);
100
- }
82
+ if (this.currentUser) this._announceUser();
83
+ if (this.roomId && this.currentUser) this._emitJoin(this.roomId);
101
84
  });
102
85
 
103
- // ✅ Setup handlers only ONCE per socket instance
104
86
  if (!this._handlersSetup) {
105
87
  this.setupEventHandlers();
106
88
  this._handlersSetup = true;
@@ -112,39 +94,68 @@ class WebSocketManager extends EventEmitter {
112
94
  });
113
95
  }
114
96
 
115
- // ─── Event Handlers (registered once) ────────────────────────────────────
97
+ // ─── Announce user to server (thinknsh:connected) ─────────────────────────
98
+
99
+ _announceUser() {
100
+ if (!this.socket || !this.currentUser) return;
101
+ this.socket.emit('thinknsh:connected', {
102
+ userId: this.currentUser.userId,
103
+ userName: this.currentUser.name,
104
+ shellToken: this._authToken,
105
+ });
106
+ }
107
+
108
+ // ─── Set user — called right after login ──────────────────────────────────
109
+
110
+ /**
111
+ * Accepts any of these shapes and normalises to { userId, name, userType }:
112
+ * { _id, userId, name, email, userType } ← from api-client.getUser()
113
+ * { userId, name, userType } ← legacy
114
+ */
115
+ setUser(user, token = null) {
116
+ if (!user) return;
117
+
118
+ if (token) this._authToken = token;
119
+
120
+ // Normalise — support both _id and userId
121
+ const id = (user.userId || user._id || '').toString();
122
+ this.currentUser = {
123
+ userId: id,
124
+ name: user.name || user.email || 'Unknown',
125
+ userType: user.userType || user.model || user.role || 'User',
126
+ };
127
+
128
+ // If already connected, announce immediately
129
+ if (this.connected && this.socket) {
130
+ this._announceUser();
131
+ }
132
+ }
133
+
134
+ // ─── Event Handlers ───────────────────────────────────────────────────────
116
135
 
117
136
  setupEventHandlers() {
118
137
 
119
- // Terminal ready confirmed by server
120
138
  this.socket.on('thinknsh:ready', (data) => {
139
+ this._serverReady = true;
121
140
  this.emit('ready', data);
122
141
  });
123
142
 
124
- // ✅ Room join confirmed + history delivered
125
143
  this.socket.on('thinknsh:joined', (data) => {
126
144
  this.roomId = data.roomId;
127
-
128
- // Emit history separately so shell.js can display it
129
- if (data.history && data.history.length > 0) {
130
- this.emit('messageHistory', data.history);
131
- }
132
-
145
+ if (data.history?.length) this.emit('messageHistory', data.history);
133
146
  this.emit('roomJoined', data);
134
147
  });
135
148
 
136
- // Incoming message from terminal users
137
149
  this.socket.on('thinknsh:message', (data) => {
138
150
  this.emit('message', {
139
151
  username: data.name,
140
152
  userId: data.userId,
141
153
  message: data.message,
142
- source: data.source || 'shell',
154
+ source: 'shell',
143
155
  timestamp: data.timestamp,
144
156
  });
145
157
  });
146
158
 
147
- // Incoming message from web users
148
159
  this.socket.on('chat-message', (data) => {
149
160
  this.emit('message', {
150
161
  username: data.name,
@@ -155,75 +166,55 @@ class WebSocketManager extends EventEmitter {
155
166
  });
156
167
  });
157
168
 
158
- // User joined from terminal
159
169
  this.socket.on('thinknsh:userJoined', (data) => {
160
170
  this.emit('userJoined', { username: data.name, source: 'shell' });
161
171
  });
162
172
 
163
- // ✅ User left from terminal
164
173
  this.socket.on('thinknsh:userLeft', (data) => {
165
174
  this.emit('userLeft', { username: data.name, source: 'shell' });
166
175
  });
167
176
 
168
- // User joined from web
169
177
  this.socket.on('user-joined', (name) => {
170
178
  this.emit('userJoined', { username: name, source: 'web' });
171
179
  });
172
180
 
173
- // User left from web
174
181
  this.socket.on('user-left', (name) => {
175
182
  this.emit('userLeft', { username: name, source: 'web' });
176
183
  });
177
184
 
178
- // User list updated
179
185
  this.socket.on('user-list', (users) => {
180
186
  this.emit('userList', { users });
181
187
  });
182
188
 
183
- // ✅ Manual history fetch response
184
189
  this.socket.on('thinknsh:history', (data) => {
185
- if (data.messages && data.messages.length > 0) {
186
- this.emit('messageHistory', data.messages);
187
- } else {
188
- this.emit('messageHistory', []);
189
- }
190
+ this.emit('messageHistory', data.messages?.length ? data.messages : []);
190
191
  });
191
192
 
192
- // Single new-notification listener
193
193
  this.socket.on('new-notification', (data) => {
194
194
  this.emit('notification', {
195
- type: data.type || 'notification',
196
- level: data.type || 'info',
197
- title: data.type || 'Notification',
198
- message: data.message || '',
199
- meta: data.meta || {},
195
+ type: data.type || 'info',
196
+ level: data.type || 'info',
197
+ title: data.title || 'Notification',
198
+ message: data.message || '',
199
+ meta: data.meta || {},
200
200
  taskTitle: data.taskTitle || data.meta?.taskTitle || '',
201
201
  assignedBy: data.assignedBy || data.meta?.assignedBy || '',
202
202
  });
203
203
  });
204
204
 
205
- // Room activity feed
206
205
  this.socket.on('room:activity', (data) => {
207
- this.emit('roomActivity', {
208
- action: data.action,
209
- label: data.label,
210
- userName: data.userName,
211
- meta: data.meta || {},
212
- createdAt: data.createdAt || new Date(),
213
- });
206
+ this.emit('roomActivity', data);
214
207
  });
215
208
 
216
- // Terminal status (online/offline)
217
209
  this.socket.on('thinknsh:status', (data) => {
218
210
  this.emit('shellStatus', data);
219
211
  });
220
212
 
221
- // Notification list response
222
213
  this.socket.on('thinknsh:notifications', (data) => {
223
214
  this.emit('notificationList', data.notifications);
224
215
  });
225
216
 
226
- // Server errors
217
+ // Server errors — emit as 'serverError' so shell.js can handle separately
227
218
  this.socket.on('thinknsh:error', (data) => {
228
219
  this.emit('serverError', data);
229
220
  });
@@ -233,46 +224,64 @@ class WebSocketManager extends EventEmitter {
233
224
  });
234
225
  }
235
226
 
236
- // ─── Set user (call after login) ──────────────────────────────────────────
237
-
238
- setUser(user, token = null) {
239
- this.currentUser = user;
240
- if (token) this._authToken = token;
241
-
242
- if (this.connected && this.socket) {
243
- this.socket.emit('thinknsh:connected', {
244
- userId: user.userId,
245
- userName: user.name,
246
- shellToken: token || this._authToken,
247
- });
248
- }
249
- }
250
-
251
227
  // ─── Join room ────────────────────────────────────────────────────────────
252
228
 
253
229
  joinRoom(roomId) {
254
230
  return new Promise((resolve, reject) => {
255
- if (!this.connected || !this.socket) return reject(new Error('Not connected'));
256
- if (!this.currentUser) return reject(new Error('Not logged in'));
257
-
258
- const timeout = setTimeout(() => reject(new Error('Join timeout')), 5000);
259
-
260
- this.socket.once('thinknsh:joined', (data) => {
231
+ if (!this.connected || !this.socket)
232
+ return reject(new Error('Not connected to server'));
233
+ if (!this.currentUser)
234
+ return reject(new Error('Not logged in — run: login'));
235
+
236
+ const timeout = setTimeout(() => {
237
+ this.socket.off('thinknsh:joined', onJoined);
238
+ this.socket.off('thinknsh:error', onError);
239
+ this.socket.off('thinknsh:ready', onReady);
240
+ reject(new Error('Join timeout'));
241
+ }, 8000);
242
+
243
+ const onJoined = (data) => {
261
244
  clearTimeout(timeout);
245
+ this.socket.off('thinknsh:error', onError);
262
246
  this.roomId = data.roomId;
263
247
  resolve(data);
264
- });
248
+ };
249
+
250
+ // onReady declared at top so onError can reference it regardless of branch
251
+ let onReady = null;
265
252
 
266
- this._emitJoin(roomId);
253
+ const onError = (data) => {
254
+ clearTimeout(timeout);
255
+ this.socket.off('thinknsh:joined', onJoined);
256
+ if (onReady) this.socket.off('thinknsh:ready', onReady);
257
+ reject(new Error(data.message || 'Join error'));
258
+ };
259
+
260
+ const doJoin = () => {
261
+ this.socket.once('thinknsh:joined', onJoined);
262
+ this.socket.once('thinknsh:error', onError);
263
+ this._emitJoin(roomId);
264
+ };
265
+
266
+ if (this._serverReady) {
267
+ doJoin();
268
+ } else {
269
+ onReady = () => {
270
+ this._serverReady = true;
271
+ doJoin();
272
+ };
273
+ this.socket.once('thinknsh:ready', onReady);
274
+ }
267
275
  });
268
276
  }
269
277
 
270
278
  _emitJoin(roomId) {
279
+ if (!this.socket || !this.currentUser) return;
271
280
  this.socket.emit('thinknsh:join', {
272
281
  roomId,
273
282
  userId: this.currentUser.userId,
274
283
  name: this.currentUser.name,
275
- userType: this.currentUser.userType || 'User',
284
+ userType: this.currentUser.userType,
276
285
  });
277
286
  }
278
287
 
@@ -300,55 +309,43 @@ class WebSocketManager extends EventEmitter {
300
309
  roomId: this.roomId,
301
310
  userId: this.currentUser.userId,
302
311
  name: this.currentUser.name,
303
- userType: this.currentUser.userType || 'User',
312
+ userType: this.currentUser.userType,
304
313
  message,
305
314
  });
306
315
 
307
316
  return Promise.resolve({ success: true });
308
317
  }
309
318
 
310
- // ─── Fetch message history manually ──────────────────────────────────────
319
+ // ─── History ──────────────────────────────────────────────────────────────
311
320
 
312
321
  getHistory(limit = 30, skip = 0) {
313
- if (!this.connected || !this.socket) return;
314
- if (!this.roomId) return;
315
-
316
- this.socket.emit('thinknsh:getHistory', {
317
- roomId: this.roomId,
318
- limit,
319
- skip,
320
- });
322
+ if (!this.connected || !this.socket || !this.roomId) return;
323
+ this.socket.emit('thinknsh:getHistory', { roomId: this.roomId, limit, skip });
321
324
  }
322
325
 
323
326
  // ─── Notifications ────────────────────────────────────────────────────────
324
327
 
325
328
  getNotifications() {
326
329
  if (!this.connected || !this.currentUser) return;
327
- this.socket.emit('thinknsh:getNotifications', {
328
- userId: this.currentUser.userId
329
- });
330
+ this.socket.emit('thinknsh:getNotifications', { userId: this.currentUser.userId });
330
331
  }
331
332
 
332
- // ─── Send notification ────────────────────────────────────────────────────
333
+ sendNotification(notification, target = 'room') {
334
+ if (!this.connected || !this.socket)
335
+ return Promise.reject(new Error('Not connected'));
336
+
337
+ this.socket.emit('notification:send', {
338
+ type: notification.type || 'info',
339
+ title: notification.title || 'Notification',
340
+ message: notification.message,
341
+ data: notification.data || {},
342
+ target: target === 'room' ? this.roomId : target,
343
+ timestamp: new Date().toISOString()
344
+ });
333
345
 
334
- sendNotification(notification, target = 'room') {
335
- if (!this.connected || !this.socket) {
336
- return Promise.reject(new Error('Not connected'));
346
+ return Promise.resolve({ success: true });
337
347
  }
338
348
 
339
- const payload = {
340
- type: notification.type || 'info',
341
- title: notification.title || 'Notification',
342
- message: notification.message,
343
- data: notification.data || {},
344
- target: target === 'room' ? this.roomId : target,
345
- timestamp: new Date().toISOString()
346
- };
347
-
348
- this.socket.emit('notification:send', payload);
349
- return Promise.resolve({ success: true });
350
- }
351
-
352
349
  markNotificationsRead(notificationIds = []) {
353
350
  if (!this.connected || !this.currentUser) return;
354
351
  this.socket.emit('thinknsh:markRead', {
@@ -359,43 +356,33 @@ sendNotification(notification, target = 'room') {
359
356
 
360
357
  // ─── Helpers ──────────────────────────────────────────────────────────────
361
358
 
362
- joinUserRoom(userId) {
363
- if (!this.socket || !userId) return;
364
- this.socket.emit('thinknsh:connected', {
365
- userId,
366
- userName: this.currentUser?.name || '',
367
- shellToken: this._authToken || null,
368
- });
369
- }
370
-
371
- ping() {
372
- if (this.connected && this.socket) this.socket.emit('ping');
373
- }
374
-
375
359
  processPendingMessages() {
376
- if (this.messageQueue.length > 0 && this.roomId && this.currentUser) {
360
+ if (this.messageQueue.length && this.roomId && this.currentUser) {
377
361
  this.messageQueue.forEach(msg => this.sendMessage(msg));
378
362
  this.messageQueue = [];
379
363
  }
380
364
  }
381
365
 
382
- sendTyping() {} // add later if needed
383
-
384
366
  disconnect() {
385
367
  if (this.socket) {
368
+ this.socket.removeAllListeners();
386
369
  this.socket.disconnect();
387
370
  this.socket = null;
388
371
  this.connected = false;
389
372
  this.currentUser = null;
390
373
  this.roomId = null;
391
374
  this._handlersSetup = false;
375
+ this._serverReady = false;
376
+ this._serverReady = false;
392
377
  }
393
378
  }
394
379
 
395
- isConnected() { return this.connected && !!this.socket?.connected; }
396
- getCurrentRoom() { return this.roomId; }
397
- getSocketId() { return this.socket?.id || null; }
398
- getParticipants() { return Promise.resolve([]); }
380
+ isConnected() { return this.connected && !!this.socket?.connected; }
381
+ getCurrentRoom() { return this.roomId; }
382
+ getSocketId() { return this.socket?.id || null; }
383
+ getParticipants(){ return Promise.resolve([]); }
384
+ sendTyping() {}
385
+ ping() { if (this.connected && this.socket) this.socket.emit('ping'); }
399
386
  }
400
387
 
401
388
  module.exports = WebSocketManager;