wexa-chat 0.1.23 → 0.1.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,16 +1,24 @@
1
1
  # wexa-chat (Chat Core)
2
2
 
3
- TypeScript chat core for MongoDB with optional Redis and a built‑in WebSocket transport. Ships server APIs and a lightweight browser client bundle used by the demo app.
3
+ TypeScript chat core for MongoDB with optional Redis and a built‑in WebSocket transport. Ships server APIs and a lightweight browser client bundle.
4
4
 
5
5
  - Server: `import { initChat } from 'wexa-chat'`
6
6
  - Client: `import { SocketProvider, useSocket, getSocketManager, type ChatEvent } from 'wexa-chat/client'`
7
7
 
8
- This README mirrors the integration used in the Next.js demo app.
8
+ ## Key Features
9
+
10
+ - Single socket connection per user ID - no need to join individual conversations
11
+ - Direct communication between users via unique identifiers
12
+ - Messages are stored with conversationId for organization and retrieval
13
+ - Support for various ID types (userId, organizationId, candidateId, applicationId)
14
+ - Simplified Redis pub/sub for cross-instance communication
9
15
 
10
16
  ## Installation
11
17
 
12
18
  ```bash
13
19
  npm install wexa-chat mongoose ioredis
20
+ # or with pnpm
21
+ pnpm add wexa-chat mongoose ioredis
14
22
  ```
15
23
 
16
24
  ## Server usage
@@ -29,15 +37,13 @@ export async function getChatInstance(httpServer?: HTTPServer) {
29
37
  // Attach WS transport when server becomes available
30
38
  if (httpServer && !(chatInstance.transport as any).socket) {
31
39
  chatInstance = await initChat(mongoose, {
32
- participantModels: ['User'],
33
- memberRoles: ['member', 'admin'],
34
40
  mongoUri: process.env.MONGODB_URI!,
35
41
  // Optional Redis for cross‑instance fanout
36
42
  redis: process.env.REDIS_URL || process.env.REDIS_HOST
37
43
  ? { url: process.env.REDIS_URL, host: process.env.REDIS_HOST, port: Number(process.env.REDIS_PORT || 6379) }
38
44
  : undefined,
39
- // IMPORTANT: must match the client (recommended '/ws')
40
- socket: { path: '/ws' },
45
+ // IMPORTANT: must match the client endpoint
46
+ socket: { path: '/api/socket' },
41
47
  enableTextSearch: true,
42
48
  }, httpServer);
43
49
  }
@@ -46,13 +52,11 @@ export async function getChatInstance(httpServer?: HTTPServer) {
46
52
 
47
53
  // First‑time init; only pass socket when server exists
48
54
  chatInstance = await initChat(mongoose, {
49
- participantModels: ['User'],
50
- memberRoles: ['member', 'admin'],
51
55
  mongoUri: process.env.MONGODB_URI!,
52
56
  redis: process.env.REDIS_URL || process.env.REDIS_HOST
53
57
  ? { url: process.env.REDIS_URL, host: process.env.REDIS_HOST, port: Number(process.env.REDIS_PORT || 6379) }
54
58
  : undefined,
55
- socket: httpServer ? { path: '/ws' } : undefined,
59
+ socket: httpServer ? { path: '/api/socket' } : undefined,
56
60
  enableTextSearch: true,
57
61
  }, httpServer);
58
62
 
@@ -63,8 +67,8 @@ export async function getChatInstance(httpServer?: HTTPServer) {
63
67
  ### WebSocket transport
64
68
 
65
69
  - Transport uses native `ws` and attaches an HTTP upgrade listener when `httpServer` is provided.
66
- - Set `socket.path` to the URL path for upgrades. The client bundle expects `/ws`; configure the same on the server.
67
- - Authentication: client connects to `ws(s)://host/ws?userId=...&userModel=User`. The server validates the handshake and associates the user.
70
+ - Set `socket.path` to the URL path for upgrades. The client bundle expects `/api/socket`.
71
+ - Authentication: client connects to `ws(s)://host/api/socket?userId=...`. The server validates the handshake and associates the user.
68
72
 
69
73
  ### Next.js warm‑up route
70
74
 
@@ -85,7 +89,7 @@ export default async function handler(_req: NextApiRequest, res: NextApiResponse
85
89
  }
86
90
  ```
87
91
 
88
- The browser client performs a onetime GET of `/api/socket` before opening the WebSocket.
92
+ The browser client performs a one-time GET of `/api/socket` before opening the WebSocket.
89
93
 
90
94
  ## Options overview (see `src/types/init.ts`)
91
95
 
@@ -96,8 +100,6 @@ export type SocketConfig = {
96
100
  };
97
101
 
98
102
  export type InitOptions = {
99
- participantModels: string[]; // e.g. ['User']
100
- memberRoles: string[]; // e.g. ['member', 'admin']
101
103
  mongoUri: string; // MongoDB connection
102
104
  enableTextSearch?: boolean;
103
105
  redis?: { // optional
@@ -105,7 +107,6 @@ export type InitOptions = {
105
107
  socketConnectTimeout?: number; keepAlive?: number;
106
108
  };
107
109
  socket?: SocketConfig; // WebSocket config
108
- presence?: { enabled?: boolean; heartbeatSec?: number };
109
110
  rateLimit?: { sendPerMin?: number; typingPerMin?: number };
110
111
  }
111
112
  ```
@@ -121,127 +122,28 @@ export type InitOptions = {
121
122
  - `searchByParticipantPair({ organizationId, a, b })`
122
123
 
123
124
  - Messages
124
- - `sendMessage({ organizationId, conversationId, senderModel, senderId, text, kind?, parentMessageId?, rootThreadId? })`
125
+ - `sendMessage({ organizationId, conversationId, senderModel, senderId, text, targetUserId, kind?, parentMessageId?, rootThreadId? })`
125
126
  - `listMessages({ organizationId, conversationId, limit?, cursor? })`
126
127
  - `listMessagesWithSenders({ organizationId, conversationId, limit?, cursor?, populateSenders: true, populateOptions? })`
127
- - `markRead({ organizationId, conversationId, participantModel, participantId, messageId })`
128
128
 
129
129
  DTO types live in `src/types/dto.ts`.
130
130
 
131
- ### Populating Message Senders with Infinite Scroll Support
131
+ ### Sending Messages with Direct Routing
132
132
 
133
- The `listMessagesWithSenders` method allows you to retrieve messages with populated sender details, optimized for infinite scroll in chat interfaces. The implementation uses efficient batched lookups:
133
+ The `sendMessage` method now supports direct routing to specific users with the new `targetUserId` parameter:
134
134
 
135
135
  ```ts
136
- // Basic usage
137
- const messagesWithSenders = await chat.services.messages.listMessagesWithSenders({
136
+ // Send a message to a specific user
137
+ await chat.services.messages.sendMessage({
138
138
  organizationId: 'org-123',
139
139
  conversationId: 'conv-456',
140
- populateSenders: true // Required to enable population
141
- });
142
-
143
- // With customization
144
- const messagesWithCustomSenders = await chat.services.messages.listMessagesWithSenders({
145
- organizationId: 'org-123',
146
- conversationId: 'conv-456',
147
- populateSenders: true,
148
- populateOptions: {
149
- fields: '_id name email profilePicture', // Fields to select from sender model
150
- modelMapping: {
151
- 'User': 'CustomUserModel', // If your senderModel doesn't match the actual Mongoose model name
152
- 'Bot': 'BotModel'
153
- }
154
- }
155
- });
156
-
157
- // Accessing populated data
158
- messagesWithSenders.items.forEach(message => {
159
- console.log(`Message from ${message.sender?.name}: ${message.text}`);
140
+ senderModel: 'User',
141
+ senderId: 'user-789',
142
+ text: 'Hello there!',
143
+ targetUserId: 'org-321', // Target recipient ID (userId, organizationId, applicationId, etc.)
160
144
  });
161
145
  ```
162
146
 
163
- The populated sender data is available in the `sender` property of each message. The implementation uses a batched approach that:
164
-
165
- 1. Fetches messages using MongoDB aggregation
166
- 2. Groups messages by sender model type
167
- 3. Loads all senders for each model type in a single query per model
168
- 4. Attaches sender data to each message efficiently
169
-
170
- This approach minimizes database queries and maximizes performance, even with multiple sender types.
171
-
172
- #### Infinite Scroll Implementation
173
-
174
- The returned data structure supports infinite scroll with cursor-based pagination:
175
-
176
- ```ts
177
- // First load
178
- const firstPage = await chat.services.messages.listMessagesWithSenders({
179
- organizationId: 'org-123',
180
- conversationId: 'conv-456',
181
- populateSenders: true,
182
- limit: 20 // Number of messages per "page"
183
- });
184
-
185
- // Display messages in your UI
186
- displayMessages(firstPage.items);
187
-
188
- // When user scrolls and needs more messages
189
- if (firstPage.hasMore) {
190
- // Load next batch using the cursor
191
- const nextPage = await chat.services.messages.listMessagesWithSenders({
192
- organizationId: 'org-123',
193
- conversationId: 'conv-456',
194
- populateSenders: true,
195
- limit: 20,
196
- cursor: firstPage.nextCursor // Pass the cursor from previous batch
197
- });
198
-
199
- // Append these messages to your UI
200
- displayMessages([...existingMessages, ...nextPage.items]);
201
-
202
- // Store the new cursor for further loading
203
- nextCursor = nextPage.nextCursor;
204
- hasMore = nextPage.hasMore;
205
- }
206
- ```
207
-
208
- The method returns messages sorted by creation time (newest first), making it ideal for chat interfaces where newer messages appear at the bottom. The cursor-based pagination ensures efficient loading of large conversation histories.
209
-
210
- The type `MessageWithSender` is exported from the package for proper TypeScript support.
211
-
212
- ## Models
213
-
214
- The package exports the following model types which can be imported directly:
215
-
216
- ```ts
217
- import {
218
- // Model interfaces (document structures)
219
- type IConversation,
220
- type IMessage,
221
-
222
- // Mongoose model types
223
- type ConversationModel,
224
- type MessageModel,
225
-
226
- // Extended types
227
- type MessageWithSender // Message with populated sender data
228
- } from 'wexa-chat';
229
- ```
230
-
231
- To access the actual models:
232
-
233
- ```ts
234
- // Initialize the chat system
235
- const chat = await initChat(mongoose, options);
236
-
237
- // Access models
238
- const { Conversation, Message } = chat.models;
239
-
240
- // Example usage
241
- const conversations = await Conversation.find({ organizationId }).lean();
242
- const messages = await Message.find({ conversationId }).sort({ createdAt: -1 }).lean();
243
- ```
244
-
245
147
  ## Browser client
246
148
 
247
149
  Entry: `wexa-chat/client` exports `SocketProvider`, `useSocket`, `getSocketManager`, and `type ChatEvent`.
@@ -263,132 +165,72 @@ Use in components:
263
165
  import { useEffect } from 'react';
264
166
  import { useSocket, type ChatEvent } from 'wexa-chat/client';
265
167
 
266
- export function ChatWindow({ conversationId }: { conversationId: string }) {
168
+ export function ChatWindow({ conversationId, otherUserId }: { conversationId: string, otherUserId: string }) {
267
169
  const { isConnected, send, onEvent } = useSocket();
268
170
 
269
171
  useEffect(() => {
270
- if (!conversationId) return;
271
- if (isConnected) send({ type: 'join-conversation', conversationId });
272
- const off = onEvent((e: ChatEvent) => {
273
- if (e.type === 'message:created' && e.message?.conversationId === conversationId) {
274
- // update UI
275
- }
276
- });
277
- return off;
278
- }, [conversationId, isConnected, send, onEvent]);
279
-
280
- return null;
281
- }
282
- ```
283
-
284
- You can also send:
285
-
286
- ```ts
287
- send({ type: 'leave-conversation', conversationId });
288
- send({ type: 'typing', conversationId, isTyping: true });
289
-
290
- // New: User-based subscriptions
291
- send({ type: 'subscribe-to-user', userId: 'user-123' });
292
- send({ type: 'unsubscribe-from-user', userId: 'user-123' });
293
- ```
294
-
295
- ## User-Based Subscriptions
296
-
297
- The chat core now supports subscribing to all messages for a specific user, allowing you to receive messages from all conversations involving that user without needing to join each conversation individually.
298
-
299
- ### Server-Side Usage
300
-
301
- The transport layer exposes new methods for user-based subscriptions:
302
-
303
- ```ts
304
- // Access the transport from your chat instance
305
- const { transport } = await getChatInstance();
306
-
307
- // Subscribe a socket to all messages for a user
308
- transport.subscribeToUser(userId, socketId);
309
-
310
- // Unsubscribe a socket from a user's messages
311
- transport.unsubscribeFromUser(userId, socketId);
312
-
313
- // Check if a socket is subscribed to a user
314
- const isSubscribed = transport.isSubscribedToUser(userId, socketId);
315
-
316
- // Fan out a message to all subscribers of a user
317
- transport.fanoutToUser(userId, payload, (toSocketId, event) => {
318
- // Your emit logic here
319
- });
320
- ```
321
-
322
- ### Client-Side Usage
323
-
324
- Subscribe to all messages for a user:
325
-
326
- ```tsx
327
- import { useEffect } from 'react';
328
- import { useSocket } from 'wexa-chat/client';
329
-
330
- export function GlobalMessageListener({ userId }: { userId: string }) {
331
- const { isConnected, send, onEvent } = useSocket();
332
-
333
- // Subscribe to user messages
334
- useEffect(() => {
335
- if (isConnected && userId) {
336
- send({ type: 'subscribe-to-user', userId });
337
- }
172
+ if (!isConnected) return;
338
173
 
339
- return () => {
340
- if (isConnected && userId) {
341
- send({ type: 'unsubscribe-from-user', userId });
174
+ const off = onEvent((e: ChatEvent) => {
175
+ if (e.type === 'message:received' && e.conversationId === conversationId) {
176
+ // update UI with new message
177
+ console.log('New message:', e.message);
342
178
  }
343
- };
344
- }, [isConnected, userId, send]);
345
-
346
- // Listen for messages
347
- useEffect(() => {
348
- const off = onEvent((event) => {
349
- if (event.type === 'message:created') {
350
- // Handle message from any conversation involving the subscribed user
351
- console.log('New message:', event.message);
179
+ if (e.type === 'typing' && e.conversationId === conversationId) {
180
+ // update typing indicator
181
+ console.log('Typing status:', e.state, 'from:', e.senderId);
352
182
  }
353
183
  });
354
184
 
355
185
  return off;
356
- }, [onEvent]);
186
+ }, [conversationId, isConnected, onEvent]);
187
+
188
+ // Send a message to the other user
189
+ const sendMessage = (text: string) => {
190
+ if (isConnected) {
191
+ send({
192
+ type: 'send-message',
193
+ conversationId,
194
+ targetUserId: otherUserId,
195
+ text
196
+ });
197
+ }
198
+ };
199
+
200
+ // Send typing indicator
201
+ const sendTyping = (isTyping: boolean) => {
202
+ if (isConnected) {
203
+ send({
204
+ type: 'typing',
205
+ conversationId,
206
+ targetUserId: otherUserId,
207
+ isTyping
208
+ });
209
+ }
210
+ };
357
211
 
358
- return null; // This component just listens for messages
212
+ return null;
359
213
  }
360
214
  ```
361
215
 
362
- ### Use Cases
363
-
364
- - **Global notifications**: Receive notifications for all messages involving a user
365
- - **Unread message tracking**: Track unread messages across all conversations
366
- - **Real-time updates**: Keep conversation lists updated without joining each conversation
367
- - **Background message sync**: Sync messages even when not actively viewing conversations
368
-
369
216
  ## Events
370
217
 
371
218
  ### Client → Server
372
219
 
373
- - `join-conversation` — `{ type: 'join-conversation', conversationId }`
374
- - `leave-conversation` — `{ type: 'leave-conversation', conversationId }`
375
- - `typing` — `{ type: 'typing', conversationId, isTyping: boolean }`
376
- - `subscribe-to-user` — `{ type: 'subscribe-to-user', userId }` *(New)*
377
- - `unsubscribe-from-user` — `{ type: 'unsubscribe-from-user', userId }` *(New)*
220
+ - `send-message` — `{ type: 'send-message', conversationId, targetUserId, text }`
221
+ - `typing` — `{ type: 'typing', conversationId, targetUserId, isTyping: boolean }`
378
222
 
379
223
  ### Server → Client
380
224
 
381
- - `message:created` — `{ type: 'message:created', message }`
382
- - `conversation:read` — `{ type: 'conversation:read', conversationId, messageId, participantModel, participantId, at }`
383
- - `typing` — `{ type: 'typing', conversationId, participantModel, participantId, state: 'start' | 'stop' }`
384
- - `presence:join` / `presence:leave` — `{ type, organizationId, participantModel, participantId, at }`
225
+ - `message:received` — `{ type: 'message:received', message, conversationId, senderId }`
226
+ - `typing` — `{ type: 'typing', conversationId, senderId, state: 'start' | 'stop', at }`
385
227
 
386
228
  If Redis is configured, events are pub/sub‑broadcast across instances.
387
229
 
388
230
  ## Build
389
231
 
390
232
  ```bash
391
- npm run build
233
+ pnpm build
392
234
  ```
393
235
 
394
236
  Exports (from `package.json`):
@@ -406,8 +248,8 @@ Exports (from `package.json`):
406
248
 
407
249
  - `MONGODB_URI`
408
250
  - `REDIS_URL` or `REDIS_HOST`/`REDIS_PORT`/`REDIS_PASSWORD`/`REDIS_DB`
409
- - Ensure `socket.path` matches the client (recommended `/ws`).
251
+ - Ensure `socket.path` matches the client (recommended `/api/socket`).
410
252
 
411
253
  ## License
412
254
 
413
- MIT
255
+ MIT
@@ -1,19 +1,15 @@
1
1
  import { ReactNode } from 'react';
2
2
 
3
3
  type ChatEvent = {
4
- type: 'message:created';
5
- [k: string]: any;
6
- } | {
7
- type: 'conversation:read';
4
+ type: 'message:received';
5
+ message: any;
6
+ conversationId: string;
8
7
  [k: string]: any;
9
8
  } | {
10
9
  type: 'typing';
11
- [k: string]: any;
12
- } | {
13
- type: 'presence:join';
14
- [k: string]: any;
15
- } | {
16
- type: 'presence:leave';
10
+ conversationId: string;
11
+ senderId: string;
12
+ state: string;
17
13
  [k: string]: any;
18
14
  };
19
15
  type Listener<T> = (v: T) => void;
@@ -33,7 +33,7 @@ var SocketManager = class {
33
33
  return;
34
34
  const protocol = window.location.protocol === "https:" ? "wss" : "ws";
35
35
  const ws = new WebSocket(
36
- `${protocol}://${window.location.host}/ws?userId=${encodeURIComponent(userId)}`
36
+ `${protocol}://${window.location.host}/api/socket?userId=${encodeURIComponent(userId)}`
37
37
  );
38
38
  this.ws = ws;
39
39
  this.connecting = true;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/client/socketManager.ts","../../src/client/SocketProvider.tsx"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["export type ChatEvent =\n | { type: 'message:created'; [k: string]: any }\n | { type: 'conversation:read'; [k: string]: any }\n | { type: 'typing'; [k: string]: any }\n | { type: 'presence:join'; [k: string]: any }\n | { type: 'presence:leave'; [k: string]: any };\n\ntype Listener<T> = (v: T) => void;\n\nclass SocketManager {\n private ws: WebSocket | null = null;\n private reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n private manualClose = false;\n private attempt = 0;\n private warmedUp = false;\n private connecting = false;\n\n private connListeners = new Set<Listener<boolean>>();\n private eventListeners = new Set<Listener<ChatEvent>>();\n\n private getUserId: () => string | undefined = () => undefined;\n\n constructor(getUserId?: () => string | undefined) {\n if (getUserId) this.getUserId = getUserId;\n }\n\n setGetUserId = (fn: () => string | undefined) => {\n this.getUserId = fn;\n };\n\n private notifyConn(v: boolean) {\n this.connListeners.forEach((l) => l(v));\n }\n private notifyEvent(e: ChatEvent) {\n this.eventListeners.forEach((l) => l(e));\n }\n\n addConnListener(l: Listener<boolean>) {\n this.connListeners.add(l);\n return () => this.connListeners.delete(l);\n }\n addEventListener(l: Listener<ChatEvent>) {\n this.eventListeners.add(l);\n return () => this.eventListeners.delete(l);\n }\n\n connect = async () => {\n const userId = this.getUserId();\n if (!userId || typeof window === 'undefined') return;\n\n // Prevent overlapping connect attempts\n if (this.connecting) return;\n\n if (!this.warmedUp) {\n try {\n await fetch('/api/socket', { method: 'GET', keepalive: true });\n } catch {}\n this.warmedUp = true;\n }\n\n if (\n this.ws &&\n (this.ws.readyState === WebSocket.OPEN ||\n this.ws.readyState === WebSocket.CONNECTING ||\n this.ws.readyState === WebSocket.CLOSING)\n )\n return;\n\n const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws';\n const ws = new WebSocket(\n `${protocol}://${window.location.host}/ws?userId=${encodeURIComponent(userId)}`,\n );\n this.ws = ws;\n this.connecting = true;\n\n ws.onopen = () => {\n if (this.ws !== ws) return;\n this.attempt = 0;\n this.manualClose = false;\n this.notifyConn(true);\n this.connecting = false;\n };\n ws.onmessage = (msg) => {\n if (this.ws !== ws) return;\n try {\n this.notifyEvent(JSON.parse(msg.data as string));\n } catch {}\n };\n ws.onclose = () => {\n if (this.ws !== ws) return;\n this.notifyConn(false);\n this.ws = null;\n this.connecting = false;\n if (this.manualClose) return;\n this.attempt += 1;\n const delay = Math.min(5000, 500 * Math.pow(2, this.attempt));\n if (this.reconnectTimer) clearTimeout(this.reconnectTimer);\n this.reconnectTimer = setTimeout(this.connect, delay);\n };\n ws.onerror = () => {\n try {\n ws.close();\n } catch {}\n if (this.ws === ws) {\n this.connecting = false;\n }\n };\n };\n\n send = (payload: unknown) => {\n if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return false;\n try {\n this.ws.send(JSON.stringify(payload));\n return true;\n } catch {\n return false;\n }\n };\n\n close = () => {\n this.manualClose = true;\n if (this.reconnectTimer) clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n try {\n this.ws?.close();\n } catch {}\n this.ws = null;\n };\n}\n\nexport default SocketManager;\n\n// Singleton accessor that survives Next.js Fast Refresh by storing on globalThis\ndeclare const globalThis: typeof global & {\n __socketManagerSingleton?: SocketManager | null;\n};\n\nexport function getSocketManager(getUserId?: () => string | undefined) {\n if (!globalThis.__socketManagerSingleton) {\n globalThis.__socketManagerSingleton = new SocketManager(getUserId);\n } else if (getUserId) {\n globalThis.__socketManagerSingleton.setGetUserId(getUserId);\n }\n return globalThis.__socketManagerSingleton;\n}\n","'use client';\n\nimport { createContext, useContext, useEffect, useMemo, useState, useCallback, type ReactNode } from 'react';\nimport { getSocketManager } from './socketManager';\nimport type { ChatEvent } from './socketManager';\n\nexport type SocketCtxShape = {\n isConnected: boolean;\n send: (payload: unknown) => boolean;\n onEvent: (cb: (e: ChatEvent) => void) => () => void;\n};\n\nconst SocketCtx = createContext<SocketCtxShape | null>(null);\n\nexport function SocketProvider({\n children,\n getUserId,\n}: {\n children: ReactNode;\n getUserId: () => string | undefined;\n}) {\n const [isConnected, setIsConnected] = useState(false);\n\n // Singleton manager for this tab\n const mgr = useMemo(() => getSocketManager(getUserId), [getUserId]);\n\n useEffect(() => {\n // keep user id up to date and ensure connection\n mgr.setGetUserId(getUserId);\n if (getUserId()) mgr.connect();\n // Note: we intentionally DO NOT close the socket on unmount to avoid\n // duplicate connects under React Strict Mode/Fast Refresh.\n }, [mgr, getUserId]);\n\n useEffect(() => {\n const off = mgr.addConnListener(setIsConnected);\n return () => {\n off();\n };\n }, [mgr]);\n\n const sendFn = useCallback<SocketCtxShape['send']>((payload: unknown) => mgr.send(payload), [mgr]);\n const onEventFn = useCallback<SocketCtxShape['onEvent']>((cb: (e: ChatEvent) => void) => mgr.addEventListener(cb), [mgr]);\n\n const value = useMemo<SocketCtxShape>(\n () => ({\n isConnected,\n send: sendFn,\n onEvent: onEventFn,\n }),\n [isConnected, sendFn, onEventFn],\n );\n\n return <SocketCtx.Provider value={value}>{children}</SocketCtx.Provider>;\n}\n\nexport function useSocket() {\n const ctx = useContext(SocketCtx);\n if (!ctx) throw new Error('useSocket must be used within <SocketProvider />');\n return ctx;\n}\n"]}
1
+ {"version":3,"sources":["../../src/client/socketManager.ts","../../src/client/SocketProvider.tsx"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["export type ChatEvent =\n | { type: 'message:received'; message: any; conversationId: string; [k: string]: any }\n | { type: 'typing'; conversationId: string; senderId: string; state: string; [k: string]: any };\n\ntype Listener<T> = (v: T) => void;\n\nclass SocketManager {\n private ws: WebSocket | null = null;\n private reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n private manualClose = false;\n private attempt = 0;\n private warmedUp = false;\n private connecting = false;\n\n private connListeners = new Set<Listener<boolean>>();\n private eventListeners = new Set<Listener<ChatEvent>>();\n\n private getUserId: () => string | undefined = () => undefined;\n\n constructor(getUserId?: () => string | undefined) {\n if (getUserId) this.getUserId = getUserId;\n }\n\n setGetUserId = (fn: () => string | undefined) => {\n this.getUserId = fn;\n };\n\n private notifyConn(v: boolean) {\n this.connListeners.forEach((l) => l(v));\n }\n private notifyEvent(e: ChatEvent) {\n this.eventListeners.forEach((l) => l(e));\n }\n\n addConnListener(l: Listener<boolean>) {\n this.connListeners.add(l);\n return () => this.connListeners.delete(l);\n }\n addEventListener(l: Listener<ChatEvent>) {\n this.eventListeners.add(l);\n return () => this.eventListeners.delete(l);\n }\n\n connect = async () => {\n const userId = this.getUserId();\n if (!userId || typeof window === 'undefined') return;\n\n // Prevent overlapping connect attempts\n if (this.connecting) return;\n\n if (!this.warmedUp) {\n try {\n await fetch('/api/socket', { method: 'GET', keepalive: true });\n } catch {}\n this.warmedUp = true;\n }\n\n if (\n this.ws &&\n (this.ws.readyState === WebSocket.OPEN ||\n this.ws.readyState === WebSocket.CONNECTING ||\n this.ws.readyState === WebSocket.CLOSING)\n )\n return;\n\n const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws';\n const ws = new WebSocket(\n `${protocol}://${window.location.host}/api/socket?userId=${encodeURIComponent(userId)}`,\n );\n this.ws = ws;\n this.connecting = true;\n\n ws.onopen = () => {\n if (this.ws !== ws) return;\n this.attempt = 0;\n this.manualClose = false;\n this.notifyConn(true);\n this.connecting = false;\n };\n ws.onmessage = (msg) => {\n if (this.ws !== ws) return;\n try {\n this.notifyEvent(JSON.parse(msg.data as string));\n } catch {}\n };\n ws.onclose = () => {\n if (this.ws !== ws) return;\n this.notifyConn(false);\n this.ws = null;\n this.connecting = false;\n if (this.manualClose) return;\n this.attempt += 1;\n const delay = Math.min(5000, 500 * Math.pow(2, this.attempt));\n if (this.reconnectTimer) clearTimeout(this.reconnectTimer);\n this.reconnectTimer = setTimeout(this.connect, delay);\n };\n ws.onerror = () => {\n try {\n ws.close();\n } catch {}\n if (this.ws === ws) {\n this.connecting = false;\n }\n };\n };\n\n send = (payload: unknown) => {\n if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return false;\n try {\n this.ws.send(JSON.stringify(payload));\n return true;\n } catch {\n return false;\n }\n };\n\n close = () => {\n this.manualClose = true;\n if (this.reconnectTimer) clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n try {\n this.ws?.close();\n } catch {}\n this.ws = null;\n };\n}\n\nexport default SocketManager;\n\n// Singleton accessor that survives Next.js Fast Refresh by storing on globalThis\ndeclare const globalThis: typeof global & {\n __socketManagerSingleton?: SocketManager | null;\n};\n\nexport function getSocketManager(getUserId?: () => string | undefined) {\n if (!globalThis.__socketManagerSingleton) {\n globalThis.__socketManagerSingleton = new SocketManager(getUserId);\n } else if (getUserId) {\n globalThis.__socketManagerSingleton.setGetUserId(getUserId);\n }\n return globalThis.__socketManagerSingleton;\n}\n","'use client';\n\nimport { createContext, useContext, useEffect, useMemo, useState, useCallback, type ReactNode } from 'react';\nimport { getSocketManager } from './socketManager';\nimport type { ChatEvent } from './socketManager';\n\nexport type SocketCtxShape = {\n isConnected: boolean;\n send: (payload: unknown) => boolean;\n onEvent: (cb: (e: ChatEvent) => void) => () => void;\n};\n\nconst SocketCtx = createContext<SocketCtxShape | null>(null);\n\nexport function SocketProvider({\n children,\n getUserId,\n}: {\n children: ReactNode;\n getUserId: () => string | undefined;\n}) {\n const [isConnected, setIsConnected] = useState(false);\n\n // Singleton manager for this tab\n const mgr = useMemo(() => getSocketManager(getUserId), [getUserId]);\n\n useEffect(() => {\n // keep user id up to date and ensure connection\n mgr.setGetUserId(getUserId);\n if (getUserId()) mgr.connect();\n // Note: we intentionally DO NOT close the socket on unmount to avoid\n // duplicate connects under React Strict Mode/Fast Refresh.\n }, [mgr, getUserId]);\n\n useEffect(() => {\n const off = mgr.addConnListener(setIsConnected);\n return () => {\n off();\n };\n }, [mgr]);\n\n const sendFn = useCallback<SocketCtxShape['send']>((payload: unknown) => mgr.send(payload), [mgr]);\n const onEventFn = useCallback<SocketCtxShape['onEvent']>((cb: (e: ChatEvent) => void) => mgr.addEventListener(cb), [mgr]);\n\n const value = useMemo<SocketCtxShape>(\n () => ({\n isConnected,\n send: sendFn,\n onEvent: onEventFn,\n }),\n [isConnected, sendFn, onEventFn],\n );\n\n return <SocketCtx.Provider value={value}>{children}</SocketCtx.Provider>;\n}\n\nexport function useSocket() {\n const ctx = useContext(SocketCtx);\n if (!ctx) throw new Error('useSocket must be used within <SocketProvider />');\n return ctx;\n}\n"]}