@anakin824/prdg-chat-ui 0.3.0 → 0.3.2
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/dist/index.d.mts +29 -2
- package/dist/index.d.ts +29 -2
- package/dist/index.js +307 -19
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +307 -19
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -249,15 +249,42 @@ declare class ChatAPI {
|
|
|
249
249
|
}>;
|
|
250
250
|
listConversations(cursor?: string, limit?: number): Promise<ListConversationsRes>;
|
|
251
251
|
listMessages(conversationId: string, cursor?: string, limit?: number): Promise<ListMessagesRes>;
|
|
252
|
-
findOrCreateDirect(peerUserId: string
|
|
253
|
-
|
|
252
|
+
findOrCreateDirect(peerUserId: string, opts?: {
|
|
253
|
+
peerKind?: "internal" | "external";
|
|
254
|
+
}): Promise<Conversation>;
|
|
255
|
+
createGroup(title: string, members: string[], externalMembers?: string[]): Promise<Conversation>;
|
|
254
256
|
findOrCreateEntity(input: {
|
|
255
257
|
title: string;
|
|
256
258
|
entity_ref: string;
|
|
257
259
|
entity_uuid: string;
|
|
260
|
+
/** prdg-chat user UUIDs */
|
|
258
261
|
members?: string[];
|
|
262
|
+
/** Integration / HDS user ids (`ext_user_id`); resolved server-side */
|
|
263
|
+
external_members?: string[];
|
|
259
264
|
}): Promise<Conversation>;
|
|
265
|
+
/**
|
|
266
|
+
* Fetch an existing entity-linked conversation by `entity_ref` + `entity_uuid` (path).
|
|
267
|
+
* 404 if none exists; 403 if the user is not a member.
|
|
268
|
+
*/
|
|
269
|
+
getConversationByEntity(entity_ref: string, entity_uuid: string): Promise<Conversation>;
|
|
270
|
+
/** Add members to a group or entity conversation (admin only). */
|
|
271
|
+
addConversationMembers(conversationId: string, body: {
|
|
272
|
+
user_ids?: string[];
|
|
273
|
+
external_user_ids?: string[];
|
|
274
|
+
role?: "member" | "admin";
|
|
275
|
+
}): Promise<{
|
|
276
|
+
added_user_ids: string[];
|
|
277
|
+
}>;
|
|
260
278
|
listConversationMembers(conversationId: string): Promise<AppUser[]>;
|
|
279
|
+
/**
|
|
280
|
+
* Remove a member by prdg-chat user id, or by integration `ext_user_id`
|
|
281
|
+
* (`DELETE .../members/external/:external_user_id`).
|
|
282
|
+
*/
|
|
283
|
+
removeConversationMember(conversationId: string, by: {
|
|
284
|
+
userId: string;
|
|
285
|
+
} | {
|
|
286
|
+
externalUserId: string;
|
|
287
|
+
}): Promise<void>;
|
|
261
288
|
listContacts(q?: string, limit?: number): Promise<AppUser[]>;
|
|
262
289
|
sendMessage(conversationId: string, body: {
|
|
263
290
|
body?: string | null;
|
package/dist/index.d.ts
CHANGED
|
@@ -249,15 +249,42 @@ declare class ChatAPI {
|
|
|
249
249
|
}>;
|
|
250
250
|
listConversations(cursor?: string, limit?: number): Promise<ListConversationsRes>;
|
|
251
251
|
listMessages(conversationId: string, cursor?: string, limit?: number): Promise<ListMessagesRes>;
|
|
252
|
-
findOrCreateDirect(peerUserId: string
|
|
253
|
-
|
|
252
|
+
findOrCreateDirect(peerUserId: string, opts?: {
|
|
253
|
+
peerKind?: "internal" | "external";
|
|
254
|
+
}): Promise<Conversation>;
|
|
255
|
+
createGroup(title: string, members: string[], externalMembers?: string[]): Promise<Conversation>;
|
|
254
256
|
findOrCreateEntity(input: {
|
|
255
257
|
title: string;
|
|
256
258
|
entity_ref: string;
|
|
257
259
|
entity_uuid: string;
|
|
260
|
+
/** prdg-chat user UUIDs */
|
|
258
261
|
members?: string[];
|
|
262
|
+
/** Integration / HDS user ids (`ext_user_id`); resolved server-side */
|
|
263
|
+
external_members?: string[];
|
|
259
264
|
}): Promise<Conversation>;
|
|
265
|
+
/**
|
|
266
|
+
* Fetch an existing entity-linked conversation by `entity_ref` + `entity_uuid` (path).
|
|
267
|
+
* 404 if none exists; 403 if the user is not a member.
|
|
268
|
+
*/
|
|
269
|
+
getConversationByEntity(entity_ref: string, entity_uuid: string): Promise<Conversation>;
|
|
270
|
+
/** Add members to a group or entity conversation (admin only). */
|
|
271
|
+
addConversationMembers(conversationId: string, body: {
|
|
272
|
+
user_ids?: string[];
|
|
273
|
+
external_user_ids?: string[];
|
|
274
|
+
role?: "member" | "admin";
|
|
275
|
+
}): Promise<{
|
|
276
|
+
added_user_ids: string[];
|
|
277
|
+
}>;
|
|
260
278
|
listConversationMembers(conversationId: string): Promise<AppUser[]>;
|
|
279
|
+
/**
|
|
280
|
+
* Remove a member by prdg-chat user id, or by integration `ext_user_id`
|
|
281
|
+
* (`DELETE .../members/external/:external_user_id`).
|
|
282
|
+
*/
|
|
283
|
+
removeConversationMember(conversationId: string, by: {
|
|
284
|
+
userId: string;
|
|
285
|
+
} | {
|
|
286
|
+
externalUserId: string;
|
|
287
|
+
}): Promise<void>;
|
|
261
288
|
listContacts(q?: string, limit?: number): Promise<AppUser[]>;
|
|
262
289
|
sendMessage(conversationId: string, body: {
|
|
263
290
|
body?: string | null;
|
package/dist/index.js
CHANGED
|
@@ -66,16 +66,18 @@ var ChatAPI = class {
|
|
|
66
66
|
q.set("limit", String(limit));
|
|
67
67
|
return this.json(`/conversations/${conversationId}/messages?${q}`);
|
|
68
68
|
}
|
|
69
|
-
findOrCreateDirect(peerUserId) {
|
|
69
|
+
findOrCreateDirect(peerUserId, opts) {
|
|
70
|
+
const kind = opts?.peerKind ?? "internal";
|
|
71
|
+
const body = kind === "external" ? { external_user_uuid: peerUserId } : { peer_user_id: peerUserId };
|
|
70
72
|
return this.json("/conversations/direct", {
|
|
71
73
|
method: "POST",
|
|
72
|
-
body: JSON.stringify(
|
|
74
|
+
body: JSON.stringify(body)
|
|
73
75
|
});
|
|
74
76
|
}
|
|
75
|
-
createGroup(title, members) {
|
|
77
|
+
createGroup(title, members, externalMembers = []) {
|
|
76
78
|
return this.json("/conversations/group", {
|
|
77
79
|
method: "POST",
|
|
78
|
-
body: JSON.stringify({ title, members })
|
|
80
|
+
body: JSON.stringify({ title, members, external_members: externalMembers })
|
|
79
81
|
});
|
|
80
82
|
}
|
|
81
83
|
findOrCreateEntity(input) {
|
|
@@ -85,13 +87,58 @@ var ChatAPI = class {
|
|
|
85
87
|
title: input.title,
|
|
86
88
|
entity_ref: input.entity_ref,
|
|
87
89
|
entity_uuid: input.entity_uuid,
|
|
88
|
-
members: input.members ?? []
|
|
90
|
+
members: input.members ?? [],
|
|
91
|
+
external_members: input.external_members ?? []
|
|
92
|
+
})
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Fetch an existing entity-linked conversation by `entity_ref` + `entity_uuid` (path).
|
|
97
|
+
* 404 if none exists; 403 if the user is not a member.
|
|
98
|
+
*/
|
|
99
|
+
getConversationByEntity(entity_ref, entity_uuid) {
|
|
100
|
+
return this.json(
|
|
101
|
+
`/conversations/by-entity/ref/${encodeURIComponent(entity_ref)}/entity/${encodeURIComponent(entity_uuid)}`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
/** Add members to a group or entity conversation (admin only). */
|
|
105
|
+
addConversationMembers(conversationId, body) {
|
|
106
|
+
return this.json(`/conversations/${conversationId}/members`, {
|
|
107
|
+
method: "POST",
|
|
108
|
+
body: JSON.stringify({
|
|
109
|
+
user_ids: body.user_ids ?? [],
|
|
110
|
+
external_user_ids: body.external_user_ids ?? [],
|
|
111
|
+
role: body.role
|
|
89
112
|
})
|
|
90
113
|
});
|
|
91
114
|
}
|
|
92
115
|
listConversationMembers(conversationId) {
|
|
93
116
|
return this.json(`/conversations/${conversationId}/members`);
|
|
94
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Remove a member by prdg-chat user id, or by integration `ext_user_id`
|
|
120
|
+
* (`DELETE .../members/external/:external_user_id`).
|
|
121
|
+
*/
|
|
122
|
+
async removeConversationMember(conversationId, by) {
|
|
123
|
+
const path = "externalUserId" in by ? `/conversations/${conversationId}/members/external/${encodeURIComponent(by.externalUserId)}` : `/conversations/${conversationId}/members/${by.userId}`;
|
|
124
|
+
let res = await fetch(`${this.baseUrl}${path}`, {
|
|
125
|
+
method: "DELETE",
|
|
126
|
+
headers: this.headers()
|
|
127
|
+
});
|
|
128
|
+
if (res.status === 401 && this.onAuthError) {
|
|
129
|
+
const newToken = await this.onAuthError();
|
|
130
|
+
if (newToken) {
|
|
131
|
+
res = await fetch(`${this.baseUrl}${path}`, {
|
|
132
|
+
method: "DELETE",
|
|
133
|
+
headers: this.headers(newToken)
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (!res.ok) {
|
|
138
|
+
const err = await res.text();
|
|
139
|
+
throw new Error(err || res.statusText);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
95
142
|
listContacts(q, limit = 50) {
|
|
96
143
|
const params = new URLSearchParams();
|
|
97
144
|
params.set("limit", String(limit));
|
|
@@ -180,22 +227,200 @@ function userInboxSubject(natsTenantId, userId) {
|
|
|
180
227
|
function entityConversationSubject(natsTenantId, conversationId) {
|
|
181
228
|
return `chat.${natsTenantId}.conversation.${conversationId}`;
|
|
182
229
|
}
|
|
183
|
-
function
|
|
184
|
-
if (
|
|
230
|
+
function parseMessage(raw) {
|
|
231
|
+
if (typeof raw.id !== "string" || typeof raw.conversation_id !== "string") {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
id: raw.id,
|
|
236
|
+
conversation_id: raw.conversation_id,
|
|
237
|
+
sender_id: typeof raw.sender_id === "string" ? raw.sender_id : "",
|
|
238
|
+
body: typeof raw.body === "string" ? raw.body : null,
|
|
239
|
+
created_at: typeof raw.created_at === "string" ? raw.created_at : (/* @__PURE__ */ new Date()).toISOString(),
|
|
240
|
+
edited_at: typeof raw.edited_at === "string" ? raw.edited_at : null,
|
|
241
|
+
deleted_at: typeof raw.deleted_at === "string" ? raw.deleted_at : null,
|
|
242
|
+
attachments: Array.isArray(raw.attachments) ? raw.attachments : [],
|
|
243
|
+
mentioned_user_ids: Array.isArray(raw.mentioned_user_ids) ? raw.mentioned_user_ids : []
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
function getMessagePageSummary(queryClient, conversationId) {
|
|
247
|
+
const page = queryClient.getQueryData(chatKeys.messages(conversationId));
|
|
248
|
+
const lastItem = page && page.items.length > 0 ? page.items[page.items.length - 1] : null;
|
|
249
|
+
return {
|
|
250
|
+
count: page?.items.length ?? 0,
|
|
251
|
+
firstId: page?.items[0]?.id ?? null,
|
|
252
|
+
lastId: lastItem?.id ?? null
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
function pushMessageToCache(queryClient, msg) {
|
|
256
|
+
let result = "missing-cache";
|
|
257
|
+
queryClient.setQueryData(chatKeys.messages(msg.conversation_id), (prev) => {
|
|
258
|
+
if (!prev) return void 0;
|
|
259
|
+
if (prev.items.some((m) => m.id === msg.id)) {
|
|
260
|
+
result = "duplicate";
|
|
261
|
+
return prev;
|
|
262
|
+
}
|
|
263
|
+
result = "updated";
|
|
264
|
+
return { ...prev, items: [msg, ...prev.items] };
|
|
265
|
+
});
|
|
266
|
+
return result;
|
|
267
|
+
}
|
|
268
|
+
function markDeletedInCache(queryClient, conversationId, messageId, deletedAt) {
|
|
269
|
+
let result = "missing-cache";
|
|
270
|
+
queryClient.setQueryData(
|
|
271
|
+
chatKeys.messages(conversationId),
|
|
272
|
+
(prev) => {
|
|
273
|
+
if (!prev) return void 0;
|
|
274
|
+
const idx = prev.items.findIndex((m) => m.id === messageId);
|
|
275
|
+
if (idx === -1) {
|
|
276
|
+
result = "missing-message";
|
|
277
|
+
return prev;
|
|
278
|
+
}
|
|
279
|
+
const updated = [...prev.items];
|
|
280
|
+
updated[idx] = { ...updated[idx], deleted_at: deletedAt };
|
|
281
|
+
result = "updated";
|
|
282
|
+
return { ...prev, items: updated };
|
|
283
|
+
}
|
|
284
|
+
);
|
|
285
|
+
return result;
|
|
286
|
+
}
|
|
287
|
+
function applyEditInCache(queryClient, updated) {
|
|
288
|
+
let result = "missing-cache";
|
|
289
|
+
queryClient.setQueryData(
|
|
290
|
+
chatKeys.messages(updated.conversation_id),
|
|
291
|
+
(prev) => {
|
|
292
|
+
if (!prev) return void 0;
|
|
293
|
+
const idx = prev.items.findIndex((m) => m.id === updated.id);
|
|
294
|
+
if (idx === -1) {
|
|
295
|
+
result = "missing-message";
|
|
296
|
+
return prev;
|
|
297
|
+
}
|
|
298
|
+
const items = [...prev.items];
|
|
299
|
+
items[idx] = { ...items[idx], ...updated };
|
|
300
|
+
result = "updated";
|
|
301
|
+
return { ...prev, items };
|
|
302
|
+
}
|
|
303
|
+
);
|
|
304
|
+
return result;
|
|
305
|
+
}
|
|
306
|
+
function invalidateQueriesFromNatsPayload(payload, queryClient, debug = false) {
|
|
307
|
+
if (!payload || typeof payload !== "object") {
|
|
308
|
+
chatDebugWarn(debug, "NATS cache update ignored: payload was not an object", payload);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
185
311
|
const p = payload;
|
|
186
|
-
|
|
312
|
+
const eventType = p.type;
|
|
313
|
+
if (typeof eventType !== "string") {
|
|
314
|
+
chatDebugWarn(debug, "NATS cache update ignored: payload.type missing", p);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
187
317
|
const ed = p.event_data;
|
|
188
|
-
if (!ed || typeof ed !== "object")
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
318
|
+
if (!ed || typeof ed !== "object") {
|
|
319
|
+
chatDebugWarn(debug, "NATS cache update ignored: payload.event_data missing", {
|
|
320
|
+
eventType,
|
|
321
|
+
payload: p
|
|
322
|
+
});
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const eventData = ed;
|
|
326
|
+
const conversationId = typeof eventData.conversation_id === "string" ? eventData.conversation_id : null;
|
|
327
|
+
chatDebugLog(debug, "NATS cache update received", {
|
|
328
|
+
eventType,
|
|
329
|
+
conversationId,
|
|
330
|
+
hasMessage: !!eventData.message && typeof eventData.message === "object"
|
|
331
|
+
});
|
|
332
|
+
if (eventType === "chat.message.new") {
|
|
333
|
+
const convId = eventData.conversation_id;
|
|
334
|
+
if (typeof convId !== "string" || !convId.trim()) {
|
|
335
|
+
chatDebugWarn(debug, "NATS message.new ignored: missing conversation_id", eventData);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const before = getMessagePageSummary(queryClient, convId);
|
|
339
|
+
const msgRaw = eventData.message;
|
|
340
|
+
if (msgRaw && typeof msgRaw === "object") {
|
|
341
|
+
const raw = msgRaw;
|
|
342
|
+
const msg = parseMessage(raw);
|
|
343
|
+
if (msg) {
|
|
344
|
+
const mutation = pushMessageToCache(queryClient, msg);
|
|
345
|
+
const after = getMessagePageSummary(queryClient, convId);
|
|
346
|
+
chatDebugLog(debug, "NATS message.new cache mutation", {
|
|
347
|
+
conversationId: convId,
|
|
348
|
+
messageId: msg.id,
|
|
349
|
+
mutation,
|
|
350
|
+
before,
|
|
351
|
+
after
|
|
352
|
+
});
|
|
353
|
+
} else {
|
|
354
|
+
chatDebugWarn(debug, "NATS message.new ignored: event_data.message missing required ids", {
|
|
355
|
+
conversationId: convId,
|
|
356
|
+
message: raw
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
} else {
|
|
360
|
+
chatDebugWarn(debug, "NATS message.new ignored: event_data.message missing or not an object", {
|
|
361
|
+
conversationId: convId,
|
|
362
|
+
eventData
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
chatDebugLog(debug, "NATS message.new invalidating queries", {
|
|
366
|
+
conversationId: convId,
|
|
367
|
+
messageQueryKey: chatKeys.messages(convId),
|
|
368
|
+
conversationsQueryKey: chatKeys.conversations()
|
|
369
|
+
});
|
|
370
|
+
void queryClient.invalidateQueries({ queryKey: chatKeys.messages(convId) });
|
|
371
|
+
void queryClient.invalidateQueries({ queryKey: chatKeys.conversations() });
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (eventType === "chat.message.edited") {
|
|
375
|
+
const msgRaw = eventData.message;
|
|
376
|
+
if (msgRaw && typeof msgRaw === "object") {
|
|
377
|
+
const raw = msgRaw;
|
|
378
|
+
const updated = parseMessage(raw);
|
|
379
|
+
if (updated) {
|
|
380
|
+
const mutation = applyEditInCache(queryClient, updated);
|
|
381
|
+
chatDebugLog(debug, "NATS message.edited cache mutation", {
|
|
382
|
+
conversationId: updated.conversation_id,
|
|
383
|
+
messageId: updated.id,
|
|
384
|
+
mutation
|
|
385
|
+
});
|
|
386
|
+
void queryClient.invalidateQueries({ queryKey: chatKeys.messages(updated.conversation_id) });
|
|
387
|
+
} else {
|
|
388
|
+
chatDebugWarn(debug, "NATS message.edited ignored: event_data.message missing required ids", raw);
|
|
389
|
+
}
|
|
390
|
+
} else {
|
|
391
|
+
chatDebugWarn(debug, "NATS message.edited ignored: event_data.message missing or not an object", eventData);
|
|
392
|
+
}
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
if (eventType === "chat.message.deleted") {
|
|
396
|
+
const msgRaw = eventData.message;
|
|
397
|
+
if (msgRaw && typeof msgRaw === "object") {
|
|
398
|
+
const raw = msgRaw;
|
|
399
|
+
const msgId = raw.id;
|
|
400
|
+
const convId = raw.conversation_id;
|
|
401
|
+
const deletedAt = typeof raw.deleted_at === "string" ? raw.deleted_at : (/* @__PURE__ */ new Date()).toISOString();
|
|
402
|
+
if (typeof msgId === "string" && typeof convId === "string") {
|
|
403
|
+
const mutation = markDeletedInCache(queryClient, convId, msgId, deletedAt);
|
|
404
|
+
chatDebugLog(debug, "NATS message.deleted cache mutation", {
|
|
405
|
+
conversationId: convId,
|
|
406
|
+
messageId: msgId,
|
|
407
|
+
mutation
|
|
408
|
+
});
|
|
409
|
+
void queryClient.invalidateQueries({ queryKey: chatKeys.messages(convId) });
|
|
410
|
+
} else {
|
|
411
|
+
chatDebugWarn(debug, "NATS message.deleted ignored: event_data.message missing required ids", raw);
|
|
412
|
+
}
|
|
413
|
+
} else {
|
|
414
|
+
chatDebugWarn(debug, "NATS message.deleted ignored: event_data.message missing or not an object", eventData);
|
|
415
|
+
}
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
chatDebugLog(debug, "NATS payload ignored: unsupported event type", { eventType, eventData });
|
|
193
419
|
}
|
|
194
420
|
|
|
195
421
|
// src/chat/provider/ChatNatsBridge.tsx
|
|
196
422
|
function ChatNatsBridge({ natsWsUrl, natsToken, onConnectedChange }) {
|
|
197
|
-
const { natsTenantId, userId } = useChat();
|
|
198
|
-
const queryClient = reactQuery.useQueryClient();
|
|
423
|
+
const { natsTenantId, userId, queryClient, debug } = useChat();
|
|
199
424
|
const { data: convData } = useConversations();
|
|
200
425
|
const entityConversationIds = react.useMemo(() => {
|
|
201
426
|
const items = convData?.items ?? [];
|
|
@@ -208,6 +433,12 @@ function ChatNatsBridge({ natsWsUrl, natsToken, onConnectedChange }) {
|
|
|
208
433
|
const connRef = react.useRef(null);
|
|
209
434
|
react.useEffect(() => {
|
|
210
435
|
if (servers.length === 0) return;
|
|
436
|
+
chatDebugLog(debug, "ChatNatsBridge: starting subscriptions", {
|
|
437
|
+
userId,
|
|
438
|
+
natsTenantId,
|
|
439
|
+
servers,
|
|
440
|
+
entityConversationIds
|
|
441
|
+
});
|
|
211
442
|
let cancelled = false;
|
|
212
443
|
void (async () => {
|
|
213
444
|
try {
|
|
@@ -226,10 +457,21 @@ function ChatNatsBridge({ natsWsUrl, natsToken, onConnectedChange }) {
|
|
|
226
457
|
}
|
|
227
458
|
connRef.current = conn;
|
|
228
459
|
onConnectedChangeRef.current(true);
|
|
460
|
+
chatDebugLog(debug, "ChatNatsBridge: NATS connected", {
|
|
461
|
+
userId,
|
|
462
|
+
natsTenantId,
|
|
463
|
+
servers
|
|
464
|
+
});
|
|
229
465
|
const handleMsg = (data, subject) => {
|
|
466
|
+
const payloadText = new TextDecoder().decode(data);
|
|
230
467
|
if (process.env.NODE_ENV === "development") {
|
|
231
468
|
console.debug("[NATS] raw message received on subject:", subject, "bytes:", data.length);
|
|
232
469
|
}
|
|
470
|
+
chatDebugLog(debug, "ChatNatsBridge: NATS payload received", {
|
|
471
|
+
subject,
|
|
472
|
+
bytes: data.length,
|
|
473
|
+
payloadText
|
|
474
|
+
});
|
|
233
475
|
let payload;
|
|
234
476
|
try {
|
|
235
477
|
payload = jc.decode(data);
|
|
@@ -242,11 +484,12 @@ function ChatNatsBridge({ natsWsUrl, natsToken, onConnectedChange }) {
|
|
|
242
484
|
if (process.env.NODE_ENV === "development") {
|
|
243
485
|
console.debug("[NATS] decoded payload:", payload);
|
|
244
486
|
}
|
|
245
|
-
invalidateQueriesFromNatsPayload(payload, queryClient);
|
|
487
|
+
invalidateQueriesFromNatsPayload(payload, queryClient, debug);
|
|
246
488
|
if (process.env.NODE_ENV === "development") {
|
|
247
489
|
if (payload && typeof payload === "object" && payload.type === "chat.message.new") {
|
|
248
490
|
const p = payload;
|
|
249
491
|
const ed = p.event_data;
|
|
492
|
+
const msgRaw = ed?.message;
|
|
250
493
|
const rawTs = typeof ed?.timestamp_utc === "string" ? ed.timestamp_utc : null;
|
|
251
494
|
const localTime = rawTs ? new Date(rawTs).toLocaleTimeString(void 0, {
|
|
252
495
|
hour: "numeric",
|
|
@@ -261,6 +504,8 @@ function ChatNatsBridge({ natsWsUrl, natsToken, onConnectedChange }) {
|
|
|
261
504
|
`
|
|
262
505
|
conversation: ${typeof ed?.conversation_id === "string" ? ed.conversation_id : "?"}`,
|
|
263
506
|
`
|
|
507
|
+
message_id: ${typeof msgRaw?.id === "string" ? msgRaw.id : "(no id \u2014 cache push skipped)"}`,
|
|
508
|
+
`
|
|
264
509
|
sender: ${typeof ed?.sender_id === "string" ? ed.sender_id : "?"}`,
|
|
265
510
|
`
|
|
266
511
|
body: ${typeof ed?.body === "string" ? ed.body.slice(0, 200) : "(no body)"}`,
|
|
@@ -277,6 +522,7 @@ function ChatNatsBridge({ natsWsUrl, natsToken, onConnectedChange }) {
|
|
|
277
522
|
}
|
|
278
523
|
};
|
|
279
524
|
const inbox = userInboxSubject(natsTenantId, userId);
|
|
525
|
+
chatDebugLog(debug, "ChatNatsBridge: subscribing inbox", { inbox });
|
|
280
526
|
const subInbox = conn.subscribe(inbox);
|
|
281
527
|
void (async () => {
|
|
282
528
|
for await (const m of subInbox) {
|
|
@@ -286,6 +532,10 @@ function ChatNatsBridge({ natsWsUrl, natsToken, onConnectedChange }) {
|
|
|
286
532
|
})();
|
|
287
533
|
for (const convId of entityConversationIds) {
|
|
288
534
|
const subj = entityConversationSubject(natsTenantId, convId);
|
|
535
|
+
chatDebugLog(debug, "ChatNatsBridge: subscribing entity conversation", {
|
|
536
|
+
conversationId: convId,
|
|
537
|
+
subject: subj
|
|
538
|
+
});
|
|
289
539
|
const sub = conn.subscribe(subj);
|
|
290
540
|
void (async () => {
|
|
291
541
|
for await (const m of sub) {
|
|
@@ -309,23 +559,26 @@ function ChatNatsBridge({ natsWsUrl, natsToken, onConnectedChange }) {
|
|
|
309
559
|
}
|
|
310
560
|
await conn.closed();
|
|
311
561
|
connRef.current = null;
|
|
562
|
+
chatDebugWarn(debug, "ChatNatsBridge: NATS connection closed");
|
|
312
563
|
if (!cancelled) onConnectedChangeRef.current(false);
|
|
313
564
|
} catch (err) {
|
|
314
565
|
connRef.current = null;
|
|
315
566
|
if (process.env.NODE_ENV === "development") {
|
|
316
567
|
console.error("[NATS] WebSocket connect failed \u2014 check ws:// URL, NATS websocket block, and CORS/mixed content:", err);
|
|
317
568
|
}
|
|
569
|
+
chatDebugWarn(debug, "ChatNatsBridge: NATS connection failed", err);
|
|
318
570
|
if (!cancelled) onConnectedChangeRef.current(false);
|
|
319
571
|
}
|
|
320
572
|
})();
|
|
321
573
|
return () => {
|
|
322
574
|
cancelled = true;
|
|
575
|
+
chatDebugLog(debug, "ChatNatsBridge: cleaning up subscriptions");
|
|
323
576
|
onConnectedChangeRef.current(false);
|
|
324
577
|
const c = connRef.current;
|
|
325
578
|
connRef.current = null;
|
|
326
579
|
void c?.drain();
|
|
327
580
|
};
|
|
328
|
-
}, [servers, natsToken, natsTenantId, userId, entityKey, queryClient, entityConversationIds]);
|
|
581
|
+
}, [servers, natsToken, natsTenantId, userId, entityKey, queryClient, entityConversationIds, debug]);
|
|
329
582
|
return null;
|
|
330
583
|
}
|
|
331
584
|
|
|
@@ -701,16 +954,28 @@ function appendOlderMessages(current, older) {
|
|
|
701
954
|
// src/chat/hooks/useMessages.ts
|
|
702
955
|
var INITIAL_MESSAGE_PAGE_SIZE = 30;
|
|
703
956
|
function useMessages(conversationId) {
|
|
704
|
-
const { api, wsConnected, config, queryClient } = useChat();
|
|
957
|
+
const { api, wsConnected, config, queryClient, debug } = useChat();
|
|
705
958
|
const interval = config.pollIntervalMs ?? 3e4;
|
|
706
959
|
const [isFetchingOlder, setIsFetchingOlder] = react.useState(false);
|
|
707
960
|
const fetchingRef = react.useRef(false);
|
|
708
961
|
const query = reactQuery.useQuery({
|
|
709
962
|
queryKey: chatKeys.messages(conversationId),
|
|
710
963
|
queryFn: async () => {
|
|
964
|
+
chatDebugLog(debug, "useMessages: queryFn start", {
|
|
965
|
+
conversationId,
|
|
966
|
+
wsConnected
|
|
967
|
+
});
|
|
711
968
|
const res = await api.listMessages(conversationId, void 0, INITIAL_MESSAGE_PAGE_SIZE);
|
|
712
969
|
const prev = queryClient.getQueryData(chatKeys.messages(conversationId));
|
|
713
|
-
|
|
970
|
+
const merged = mergeMessagePages(res, prev);
|
|
971
|
+
chatDebugLog(debug, "useMessages: queryFn success", {
|
|
972
|
+
conversationId,
|
|
973
|
+
serverCount: res.items.length,
|
|
974
|
+
previousCachedCount: prev?.items.length ?? 0,
|
|
975
|
+
mergedCount: merged.items.length,
|
|
976
|
+
firstMessageId: merged.items[0]?.id ?? null
|
|
977
|
+
});
|
|
978
|
+
return merged;
|
|
714
979
|
},
|
|
715
980
|
enabled: !!conversationId,
|
|
716
981
|
refetchInterval: wsConnected ? false : interval
|
|
@@ -734,6 +999,29 @@ function useMessages(conversationId) {
|
|
|
734
999
|
}, [conversationId, queryClient, api]);
|
|
735
1000
|
const cached = queryClient.getQueryData(chatKeys.messages(conversationId));
|
|
736
1001
|
const hasMoreMessages = !!cached?.next_cursor;
|
|
1002
|
+
react.useEffect(() => {
|
|
1003
|
+
if (!conversationId) return;
|
|
1004
|
+
chatDebugLog(debug, "useMessages: hook snapshot", {
|
|
1005
|
+
conversationId,
|
|
1006
|
+
wsConnected,
|
|
1007
|
+
status: query.status,
|
|
1008
|
+
fetchStatus: query.fetchStatus,
|
|
1009
|
+
dataCount: query.data?.items.length ?? 0,
|
|
1010
|
+
cachedCount: cached?.items.length ?? 0,
|
|
1011
|
+
firstDataMessageId: query.data?.items[0]?.id ?? null,
|
|
1012
|
+
firstCachedMessageId: cached?.items[0]?.id ?? null
|
|
1013
|
+
});
|
|
1014
|
+
}, [
|
|
1015
|
+
cached?.items.length,
|
|
1016
|
+
cached?.items[0]?.id,
|
|
1017
|
+
conversationId,
|
|
1018
|
+
debug,
|
|
1019
|
+
query.data?.items.length,
|
|
1020
|
+
query.data?.items[0]?.id,
|
|
1021
|
+
query.fetchStatus,
|
|
1022
|
+
query.status,
|
|
1023
|
+
wsConnected
|
|
1024
|
+
]);
|
|
737
1025
|
return {
|
|
738
1026
|
...query,
|
|
739
1027
|
fetchOlderMessages,
|