@fluxy-chat/sdk 0.1.0 → 0.2.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,130 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import React from "react";
4
+ import { FluxyChatClient } from "./index";
5
+ import { decodeFluxyJwtPayload, jwtRefreshDelayMs } from "./jwt-utils";
6
+ import { FluxyRealtimeContext } from "./use-fluxy-chat";
7
+ async function resolveAuthToken(provider) {
8
+ if (typeof provider === "string") {
9
+ return { token: provider };
10
+ }
11
+ const result = await provider();
12
+ if (typeof result === "string") {
13
+ return { token: result };
14
+ }
15
+ return result;
16
+ }
17
+ async function fetchConnectSession(connectUrl, init) {
18
+ const res = await fetch(connectUrl, {
19
+ method: "POST",
20
+ headers: { "Content-Type": "application/json", ...(init?.headers ?? {}) },
21
+ body: JSON.stringify({}),
22
+ ...init,
23
+ });
24
+ const data = (await res.json().catch(() => ({})));
25
+ if (!res.ok) {
26
+ throw new Error(data.error ?? `Connect failed (${res.status})`);
27
+ }
28
+ if (!data.memberJwt?.trim()) {
29
+ throw new Error("Connect response did not include memberJwt");
30
+ }
31
+ return {
32
+ token: data.memberJwt,
33
+ userId: data.memberUserId,
34
+ };
35
+ }
36
+ export function FluxyRealtimeProvider({ children, workerUrl, authTokenProvider, connectUrl, connectRequestInit, userId: userIdProp, refreshBufferMs = 5 * 60 * 1000, onSessionError, }) {
37
+ const [token, setToken] = React.useState(null);
38
+ const [userId, setUserId] = React.useState(userIdProp ?? "");
39
+ const [refreshKey, setRefreshKey] = React.useState(0);
40
+ const isRefreshingRef = React.useRef(false);
41
+ const providerRef = React.useRef(authTokenProvider);
42
+ const connectUrlRef = React.useRef(connectUrl);
43
+ const connectInitRef = React.useRef(connectRequestInit);
44
+ React.useEffect(() => {
45
+ providerRef.current = authTokenProvider;
46
+ });
47
+ React.useEffect(() => {
48
+ connectUrlRef.current = connectUrl;
49
+ });
50
+ React.useEffect(() => {
51
+ connectInitRef.current = connectRequestInit;
52
+ });
53
+ React.useEffect(() => {
54
+ if (userIdProp)
55
+ setUserId(userIdProp);
56
+ }, [userIdProp]);
57
+ const refreshSession = React.useCallback(() => {
58
+ if (isRefreshingRef.current)
59
+ return;
60
+ setRefreshKey((k) => k + 1);
61
+ }, []);
62
+ React.useEffect(() => {
63
+ if (!authTokenProvider && !connectUrlRef.current)
64
+ return;
65
+ let cancelled = false;
66
+ let timer = null;
67
+ const run = async () => {
68
+ isRefreshingRef.current = true;
69
+ try {
70
+ let session;
71
+ if (connectUrlRef.current) {
72
+ session = await fetchConnectSession(connectUrlRef.current, connectInitRef.current);
73
+ }
74
+ else if (providerRef.current) {
75
+ session = await resolveAuthToken(providerRef.current);
76
+ }
77
+ else {
78
+ return;
79
+ }
80
+ if (cancelled)
81
+ return;
82
+ setToken(session.token);
83
+ const claims = decodeFluxyJwtPayload(session.token);
84
+ const resolvedUserId = session.userId ?? claims.sub ?? userIdProp ?? "";
85
+ if (resolvedUserId)
86
+ setUserId(resolvedUserId);
87
+ if (claims.exp && typeof providerRef.current !== "string") {
88
+ const delay = jwtRefreshDelayMs(claims.exp, refreshBufferMs);
89
+ timer = setTimeout(() => {
90
+ if (!cancelled)
91
+ run();
92
+ }, delay);
93
+ }
94
+ }
95
+ catch (err) {
96
+ const error = err instanceof Error ? err : new Error(String(err));
97
+ onSessionError?.(error);
98
+ }
99
+ finally {
100
+ if (!cancelled)
101
+ isRefreshingRef.current = false;
102
+ }
103
+ };
104
+ void run();
105
+ return () => {
106
+ cancelled = true;
107
+ isRefreshingRef.current = false;
108
+ if (timer)
109
+ clearTimeout(timer);
110
+ };
111
+ }, [authTokenProvider, connectUrl, connectRequestInit, refreshBufferMs, refreshKey, userIdProp, onSessionError]);
112
+ const client = React.useMemo(() => {
113
+ if (!token?.trim() || !userId.trim())
114
+ return null;
115
+ return new FluxyChatClient({
116
+ baseUrl: workerUrl.replace(/\/+$/, ""),
117
+ userId,
118
+ token,
119
+ });
120
+ }, [workerUrl, userId, token]);
121
+ const value = React.useMemo(() => ({
122
+ client,
123
+ userId,
124
+ token,
125
+ workerUrl,
126
+ ready: Boolean(client),
127
+ refreshSession,
128
+ }), [client, userId, token, workerUrl, refreshSession]);
129
+ return _jsx(FluxyRealtimeContext.Provider, { value: value, children: children });
130
+ }
@@ -0,0 +1,4 @@
1
+ import type { FluxyRoomMember } from "./index";
2
+ export declare const FLUXY_MAX_MESSAGE_LENGTH = 4000;
3
+ export declare function normalizeRoomMember(raw: Record<string, unknown>): FluxyRoomMember | null;
4
+ export declare function normalizeRoomMembers(rows: unknown[]): FluxyRoomMember[];
@@ -0,0 +1,24 @@
1
+ export const FLUXY_MAX_MESSAGE_LENGTH = 4000;
2
+ export function normalizeRoomMember(raw) {
3
+ const userId = String(raw.userId ?? raw.user_id ?? "").trim();
4
+ if (!userId)
5
+ return null;
6
+ const role = String(raw.role ?? "member").trim() || "member";
7
+ const joined_at = typeof raw.joined_at === "string"
8
+ ? raw.joined_at
9
+ : typeof raw.joinedAt === "string"
10
+ ? raw.joinedAt
11
+ : undefined;
12
+ return { userId, role, joined_at };
13
+ }
14
+ export function normalizeRoomMembers(rows) {
15
+ const out = [];
16
+ for (const row of rows) {
17
+ if (!row || typeof row !== "object")
18
+ continue;
19
+ const member = normalizeRoomMember(row);
20
+ if (member)
21
+ out.push(member);
22
+ }
23
+ return out;
24
+ }
@@ -0,0 +1,49 @@
1
+ import type { FluxyChatAttachment, FluxyChatClient, FluxyChatMessage } from "./index";
2
+ export interface UseChatOptions {
3
+ roomId: string;
4
+ /** Omit when wrapped in `FluxyRealtimeProvider`. */
5
+ client?: FluxyChatClient;
6
+ agentId?: string;
7
+ /** Initial REST page size (default 50). */
8
+ historyLimit?: number;
9
+ }
10
+ export declare function useChat({ roomId, client: clientProp, agentId, historyLimit }: UseChatOptions): {
11
+ messages: FluxyChatMessage[];
12
+ hasMore: boolean;
13
+ isLoadingMore: boolean;
14
+ loadMore: () => Promise<void>;
15
+ online: number;
16
+ typingUsers: Record<string, boolean>;
17
+ seenBy: Record<number, string[]>;
18
+ onlineUsers: string[];
19
+ connected: boolean;
20
+ connectionStatus: "connecting" | "connected" | "reconnecting" | "disconnected" | "polling" | "sse";
21
+ reconnectAttempt: number;
22
+ connectionError: Error | null;
23
+ agentTyping: boolean;
24
+ typingAgentId: string | null;
25
+ reactions: Record<number, Record<string, number>>;
26
+ sendMessage: (content: string, replyTo?: number | null, attachments?: FluxyChatAttachment[]) => void;
27
+ setTyping: (isTyping: boolean) => void;
28
+ editMessage: (messageId: number, content: string) => void;
29
+ sendReaction: (messageId: number, emoji: string, op?: "add" | "remove") => void;
30
+ sendReadReceipt: (messageId: number) => void;
31
+ deleteMessage: (messageId: number) => void;
32
+ invokeAgent: (content: string, options?: {
33
+ agentId?: string;
34
+ replyTo?: number | null;
35
+ }) => Promise<{
36
+ run: {
37
+ id: string;
38
+ status: string;
39
+ latencyMs?: number;
40
+ inputTokens?: number;
41
+ outputTokens?: number;
42
+ estimatedCost?: number;
43
+ iterations?: number;
44
+ toolCalls?: import("./index").FluxyChatToolCall[];
45
+ createdAt: string;
46
+ };
47
+ message: FluxyChatMessage;
48
+ }>;
49
+ };
@@ -0,0 +1,482 @@
1
+ "use client";
2
+ import React from "react";
3
+ import { FluxyAuthError, FluxySendError } from "./errors";
4
+ import { mergeMessagesChronological, sortMessagesChronological, } from "./message-history";
5
+ import { useFluxyChatOptional } from "./use-fluxy-chat";
6
+ export function useChat({ roomId, client: clientProp, agentId, historyLimit = 50 }) {
7
+ const realtime = useFluxyChatOptional();
8
+ const client = clientProp ?? realtime?.client ?? null;
9
+ const [messages, setMessages] = React.useState([]);
10
+ const [hasMore, setHasMore] = React.useState(false);
11
+ const [isLoadingMore, setIsLoadingMore] = React.useState(false);
12
+ const [online, setOnline] = React.useState(0);
13
+ const [typingUsers, setTypingUsers] = React.useState({});
14
+ const [seenBy, setSeenBy] = React.useState({});
15
+ const [onlineUsers, setOnlineUsers] = React.useState([]);
16
+ const [connected, setConnected] = React.useState(false);
17
+ const [connectionStatus, setConnectionStatus] = React.useState("connecting");
18
+ const [reconnectAttempt, setReconnectAttempt] = React.useState(0);
19
+ const [connectionError, setConnectionError] = React.useState(null);
20
+ const [agentTyping, setAgentTyping] = React.useState(false);
21
+ const [wsTypingAgentId, setWsTypingAgentId] = React.useState(null);
22
+ const [invokeTypingAgentId, setInvokeTypingAgentId] = React.useState(null);
23
+ const [reactions, setReactions] = React.useState({});
24
+ const connectionRef = React.useRef(null);
25
+ const sseRef = React.useRef(null);
26
+ const pollTimerRef = React.useRef(null);
27
+ const loadMore = React.useCallback(async () => {
28
+ if (!client || isLoadingMore || !hasMore)
29
+ return;
30
+ const trimmedRoomId = roomId.trim();
31
+ if (!trimmedRoomId)
32
+ return;
33
+ const chronological = sortMessagesChronological(messages);
34
+ const oldest = chronological[0];
35
+ if (!oldest?.createdAt)
36
+ return;
37
+ setIsLoadingMore(true);
38
+ try {
39
+ const older = await client.fetchMessages(trimmedRoomId, {
40
+ limit: historyLimit,
41
+ before: oldest.createdAt,
42
+ });
43
+ setMessages((prev) => mergeMessagesChronological(prev, older));
44
+ setHasMore(older.length >= historyLimit);
45
+ }
46
+ catch {
47
+ /* keep existing list */
48
+ }
49
+ finally {
50
+ setIsLoadingMore(false);
51
+ }
52
+ }, [client, hasMore, historyLimit, isLoadingMore, messages, roomId]);
53
+ React.useEffect(() => {
54
+ let active = true;
55
+ const trimmedRoomId = roomId.trim();
56
+ const MAX_WS_RECONNECT_ATTEMPTS = 6;
57
+ const POLL_INTERVAL_MS = 4000;
58
+ const stopPollingFallback = () => {
59
+ if (pollTimerRef.current) {
60
+ clearInterval(pollTimerRef.current);
61
+ pollTimerRef.current = null;
62
+ }
63
+ };
64
+ const stopSSEFallback = () => {
65
+ if (sseRef.current) {
66
+ sseRef.current.close();
67
+ sseRef.current = null;
68
+ }
69
+ };
70
+ const startPollingFallback = () => {
71
+ if (!client)
72
+ return;
73
+ stopPollingFallback();
74
+ stopSSEFallback();
75
+ const tick = async () => {
76
+ if (!active || !client)
77
+ return;
78
+ try {
79
+ const next = await client.fetchMessages(trimmedRoomId, { limit: historyLimit });
80
+ if (active) {
81
+ setMessages(next);
82
+ setHasMore(next.length >= historyLimit);
83
+ }
84
+ }
85
+ catch {
86
+ /* ignore transient poll errors */
87
+ }
88
+ };
89
+ void tick();
90
+ pollTimerRef.current = setInterval(tick, POLL_INTERVAL_MS);
91
+ };
92
+ const startSSEFallback = () => {
93
+ if (!client)
94
+ return;
95
+ stopPollingFallback();
96
+ stopSSEFallback();
97
+ const es = client.connectSSE(trimmedRoomId);
98
+ if (!es) {
99
+ startPollingFallback();
100
+ return;
101
+ }
102
+ sseRef.current = es;
103
+ setConnectionStatus("sse");
104
+ es.addEventListener("message", (event) => {
105
+ if (!active)
106
+ return;
107
+ try {
108
+ const data = JSON.parse(event.data);
109
+ handleEvent(data);
110
+ }
111
+ catch {
112
+ /* ignore malformed SSE events */
113
+ }
114
+ });
115
+ es.addEventListener("error", () => {
116
+ if (!active)
117
+ return;
118
+ stopSSEFallback();
119
+ startPollingFallback();
120
+ setConnectionStatus("polling");
121
+ });
122
+ };
123
+ const handleEvent = (data) => {
124
+ if (data.type === "history") {
125
+ setMessages((prev) => mergeMessagesChronological(prev, sortMessagesChronological(data.messages)));
126
+ }
127
+ else if (data.type === "message") {
128
+ setMessages((prev) => {
129
+ const idx = prev.findIndex((m) => m.id === data.id);
130
+ if (idx >= 0) {
131
+ const next = [...prev];
132
+ next[idx] = { ...next[idx], ...data };
133
+ return sortMessagesChronological(next);
134
+ }
135
+ return sortMessagesChronological([...prev, data]);
136
+ });
137
+ }
138
+ else if (data.type === "presence") {
139
+ setOnline(data.online);
140
+ if (data.users)
141
+ setOnlineUsers(data.users);
142
+ }
143
+ else if (data.type === "typing") {
144
+ setTypingUsers((prev) => ({
145
+ ...prev,
146
+ [data.userId]: data.isTyping,
147
+ }));
148
+ }
149
+ else if (data.type === "agentTyping") {
150
+ setAgentTyping(data.isTyping);
151
+ setWsTypingAgentId(data.isTyping ? data.agentId : null);
152
+ }
153
+ else if (data.type === "edit") {
154
+ setMessages((prev) => prev.map((m) => m.id === data.id
155
+ ? {
156
+ ...m,
157
+ content: data.content,
158
+ editedAt: data.editedAt,
159
+ streaming: data.streaming ?? false,
160
+ }
161
+ : m));
162
+ }
163
+ else if (data.type === "reaction") {
164
+ setReactions((prev) => {
165
+ const byMessage = { ...prev };
166
+ const current = { ...(byMessage[data.messageId] || {}) };
167
+ const existingCount = current[data.emoji] || 0;
168
+ if (data.op === "remove") {
169
+ const nextCount = Math.max(existingCount - 1, 0);
170
+ if (nextCount === 0) {
171
+ delete current[data.emoji];
172
+ }
173
+ else {
174
+ current[data.emoji] = nextCount;
175
+ }
176
+ }
177
+ else {
178
+ current[data.emoji] = existingCount + 1;
179
+ }
180
+ if (Object.keys(current).length === 0) {
181
+ delete byMessage[data.messageId];
182
+ }
183
+ else {
184
+ byMessage[data.messageId] = current;
185
+ }
186
+ return byMessage;
187
+ });
188
+ }
189
+ else if (data.type === "read") {
190
+ setSeenBy((prev) => {
191
+ const existing = prev[data.messageId] || [];
192
+ if (existing.includes(data.userId))
193
+ return prev;
194
+ return {
195
+ ...prev,
196
+ [data.messageId]: [...existing, data.userId],
197
+ };
198
+ });
199
+ }
200
+ else if (data.type === "delete") {
201
+ if (data.hard) {
202
+ setMessages((prev) => prev.filter((m) => m.id !== data.id));
203
+ }
204
+ else {
205
+ setMessages((prev) => prev.map((m) => m.id === data.id
206
+ ? { ...m, content: "[deleted]", deletedAt: data.deletedAt }
207
+ : m));
208
+ }
209
+ }
210
+ };
211
+ if (!client || !trimmedRoomId || !client.isAuthenticated()) {
212
+ setMessages([]);
213
+ setHasMore(false);
214
+ setConnected(false);
215
+ setConnectionStatus("disconnected");
216
+ return () => {
217
+ active = false;
218
+ stopPollingFallback();
219
+ stopSSEFallback();
220
+ connectionRef.current?.close();
221
+ connectionRef.current = null;
222
+ };
223
+ }
224
+ client
225
+ .fetchMessages(trimmedRoomId, { limit: historyLimit })
226
+ .then((initial) => {
227
+ if (!active)
228
+ return;
229
+ setMessages(initial);
230
+ setHasMore(initial.length >= historyLimit);
231
+ })
232
+ .catch(() => {
233
+ /* history load is best-effort until member JWT + room are ready */
234
+ });
235
+ const connection = client.connectRoom(trimmedRoomId, {
236
+ maxReconnectAttempts: MAX_WS_RECONNECT_ATTEMPTS,
237
+ historyLimit,
238
+ onStatusChange: (status) => {
239
+ if (!active)
240
+ return;
241
+ if (status === "connected") {
242
+ setConnected(true);
243
+ setConnectionStatus("connected");
244
+ setReconnectAttempt(0);
245
+ setConnectionError(null);
246
+ stopPollingFallback();
247
+ stopSSEFallback();
248
+ }
249
+ else if (status === "connecting") {
250
+ setConnectionStatus("connecting");
251
+ setConnected(false);
252
+ }
253
+ else if (status === "reconnecting") {
254
+ setConnectionStatus("reconnecting");
255
+ setConnected(false);
256
+ setReconnectAttempt(connection.reconnectAttempts);
257
+ }
258
+ else if (status === "disconnected") {
259
+ setConnected(false);
260
+ setConnectionStatus("disconnected");
261
+ }
262
+ },
263
+ onAuthError: (err) => {
264
+ if (!active)
265
+ return;
266
+ setConnectionError(err);
267
+ setConnected(false);
268
+ setConnectionStatus("disconnected");
269
+ realtime?.refreshSession();
270
+ },
271
+ onConnectionError: (err) => {
272
+ if (!active)
273
+ return;
274
+ if (!(err instanceof FluxyAuthError)) {
275
+ setConnectionError(err);
276
+ }
277
+ },
278
+ onReconnectFailed: () => {
279
+ if (!active)
280
+ return;
281
+ setReconnectAttempt(connection.reconnectAttempts);
282
+ if (client.isAuthenticated()) {
283
+ startSSEFallback();
284
+ }
285
+ else {
286
+ startPollingFallback();
287
+ }
288
+ },
289
+ });
290
+ connection.addEventListener("message", (data) => {
291
+ if (!active)
292
+ return;
293
+ handleEvent(data);
294
+ });
295
+ connectionRef.current = connection;
296
+ connection.connect();
297
+ return () => {
298
+ active = false;
299
+ stopPollingFallback();
300
+ stopSSEFallback();
301
+ connection.close();
302
+ connectionRef.current = null;
303
+ setConnected(false);
304
+ setConnectionStatus("disconnected");
305
+ };
306
+ }, [roomId, client, historyLimit, realtime?.refreshSession]);
307
+ const sendMessage = (content, replyTo, attachments) => {
308
+ if (!client)
309
+ return;
310
+ if (client.isAuthenticated()) {
311
+ void client
312
+ .createMessage(roomId, content, replyTo, attachments)
313
+ .catch((err) =>
314
+ // eslint-disable-next-line no-console
315
+ console.error("[fluxychat] REST sendMessage failed, falling back to WS:", err));
316
+ return;
317
+ }
318
+ try {
319
+ connectionRef.current?.sendJson({
320
+ type: "message",
321
+ userId: client.userId,
322
+ content,
323
+ parentId: replyTo ?? null,
324
+ attachments: attachments ?? [],
325
+ });
326
+ }
327
+ catch (err) {
328
+ if (err instanceof FluxySendError)
329
+ return;
330
+ throw err;
331
+ }
332
+ };
333
+ const setTyping = (isTyping) => {
334
+ if (!client)
335
+ return;
336
+ try {
337
+ connectionRef.current?.sendJson({
338
+ type: "typing",
339
+ userId: client.userId,
340
+ isTyping,
341
+ });
342
+ }
343
+ catch {
344
+ /* socket not open */
345
+ }
346
+ };
347
+ const editMessage = (messageId, content) => {
348
+ if (!client)
349
+ return;
350
+ const tryWsEdit = () => {
351
+ try {
352
+ connectionRef.current?.sendJson({
353
+ type: "edit",
354
+ userId: client.userId,
355
+ messageId,
356
+ content,
357
+ });
358
+ }
359
+ catch {
360
+ /* socket not open */
361
+ }
362
+ };
363
+ if (client.isAuthenticated()) {
364
+ void client.editMessageRest(messageId, content).catch((err) => {
365
+ // eslint-disable-next-line no-console
366
+ console.error("[fluxychat] REST editMessage failed, falling back to WS:", err);
367
+ tryWsEdit();
368
+ });
369
+ return;
370
+ }
371
+ tryWsEdit();
372
+ };
373
+ const sendReaction = (messageId, emoji, op = "add") => {
374
+ if (!client)
375
+ return;
376
+ if (client.isAuthenticated()) {
377
+ void client
378
+ .sendReactionRest(messageId, emoji, op)
379
+ .catch((err) =>
380
+ // eslint-disable-next-line no-console
381
+ console.error("[fluxychat] REST sendReaction failed, falling back to WS:", err));
382
+ return;
383
+ }
384
+ try {
385
+ connectionRef.current?.sendJson({
386
+ type: "reaction",
387
+ userId: client.userId,
388
+ messageId,
389
+ emoji,
390
+ op,
391
+ });
392
+ }
393
+ catch {
394
+ /* socket not open */
395
+ }
396
+ };
397
+ const sendReadReceipt = (messageId) => {
398
+ if (!client)
399
+ return;
400
+ if (client.isAuthenticated()) {
401
+ void client
402
+ .markReadRest(roomId, messageId)
403
+ .catch((err) =>
404
+ // eslint-disable-next-line no-console
405
+ console.error("[fluxychat] REST sendReadReceipt failed, falling back to WS:", err));
406
+ return;
407
+ }
408
+ try {
409
+ connectionRef.current?.sendJson({
410
+ type: "read",
411
+ userId: client.userId,
412
+ messageId,
413
+ });
414
+ }
415
+ catch {
416
+ /* socket not open */
417
+ }
418
+ };
419
+ const deleteMessage = (messageId) => {
420
+ if (!client)
421
+ return;
422
+ const tryWsDelete = () => {
423
+ try {
424
+ connectionRef.current?.sendJson({ type: "delete", messageId });
425
+ }
426
+ catch {
427
+ /* socket not open */
428
+ }
429
+ };
430
+ if (client.isAuthenticated()) {
431
+ void client.deleteMessageRest(messageId).catch((err) => {
432
+ // eslint-disable-next-line no-console
433
+ console.error("[fluxychat] REST deleteMessage failed, falling back to WS:", err);
434
+ tryWsDelete();
435
+ });
436
+ return;
437
+ }
438
+ tryWsDelete();
439
+ };
440
+ const invokeAgent = async (content, options) => {
441
+ if (!client) {
442
+ throw new Error("useChat requires a FluxyChatClient or FluxyRealtimeProvider");
443
+ }
444
+ const targetAgentId = options?.agentId || agentId;
445
+ if (!targetAgentId) {
446
+ throw new Error("invokeAgent requires an agentId in hook options or call options");
447
+ }
448
+ setAgentTyping(true);
449
+ try {
450
+ return await client.invokeAgentRest(targetAgentId, roomId, content, {
451
+ replyTo: options?.replyTo,
452
+ });
453
+ }
454
+ finally {
455
+ setAgentTyping(false);
456
+ }
457
+ };
458
+ return {
459
+ messages,
460
+ hasMore,
461
+ isLoadingMore,
462
+ loadMore,
463
+ online,
464
+ typingUsers,
465
+ seenBy,
466
+ onlineUsers,
467
+ connected,
468
+ connectionStatus,
469
+ reconnectAttempt,
470
+ connectionError,
471
+ agentTyping,
472
+ typingAgentId: wsTypingAgentId ?? invokeTypingAgentId,
473
+ reactions,
474
+ sendMessage,
475
+ setTyping,
476
+ editMessage,
477
+ sendReaction,
478
+ sendReadReceipt,
479
+ deleteMessage,
480
+ invokeAgent,
481
+ };
482
+ }