@ai-matrx/messaging 0.15.13 → 0.15.15
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/CHANGELOG.md +12 -0
- package/dist/index.cjs +446 -101
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +90 -50
- package/dist/index.d.ts +90 -50
- package/dist/index.js +446 -101
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +447 -103
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +90 -50
- package/dist/react.d.ts +90 -50
- package/dist/react.js +447 -103
- package/dist/react.js.map +1 -1
- package/package.json +2 -2
package/dist/react.js
CHANGED
|
@@ -182,6 +182,13 @@ function invalidResponse(operation, detail) {
|
|
|
182
182
|
"The database returned a shape this package does not accept. Rendering it would put a lie on the screen, so it is refused here. Fix the RPC's return contract."
|
|
183
183
|
);
|
|
184
184
|
}
|
|
185
|
+
function writeNotApplied(operation, detail) {
|
|
186
|
+
return new MessagingError(
|
|
187
|
+
"write-not-applied",
|
|
188
|
+
`${operation}: ${detail}`,
|
|
189
|
+
"The intended live row was not changed. Re-read it before retrying; the row may be gone, soft-deleted, or hidden from this session by database authorization."
|
|
190
|
+
);
|
|
191
|
+
}
|
|
185
192
|
|
|
186
193
|
// src/core/references.ts
|
|
187
194
|
var FENCE = /```matrx\s*\n([\s\S]*?)\n?```/g;
|
|
@@ -917,6 +924,7 @@ function insertOrdered(messages, message) {
|
|
|
917
924
|
}
|
|
918
925
|
function createMessagingStore() {
|
|
919
926
|
let conversations = [];
|
|
927
|
+
let conversationSummaries = /* @__PURE__ */ new Map();
|
|
920
928
|
let hasMoreConversations = false;
|
|
921
929
|
let hasLoadedConversations = false;
|
|
922
930
|
let archiveFilter = DEFAULT_MESSAGING_ARCHIVE_FILTER;
|
|
@@ -925,12 +933,15 @@ function createMessagingStore() {
|
|
|
925
933
|
let activeConversationId = null;
|
|
926
934
|
let conversationReadFailures = /* @__PURE__ */ new Map();
|
|
927
935
|
let conversationsLoadError = null;
|
|
936
|
+
const unreadRevisions = /* @__PURE__ */ new Map();
|
|
937
|
+
const confirmedReadRevisions = /* @__PURE__ */ new Map();
|
|
928
938
|
const listeners = /* @__PURE__ */ new Set();
|
|
929
939
|
let cached = null;
|
|
930
940
|
function snapshot() {
|
|
931
941
|
if (cached !== null) return cached;
|
|
932
942
|
cached = {
|
|
933
943
|
conversations,
|
|
944
|
+
conversationSummaries,
|
|
934
945
|
hasMoreConversations,
|
|
935
946
|
hasLoadedConversations,
|
|
936
947
|
threads,
|
|
@@ -949,11 +960,33 @@ function createMessagingStore() {
|
|
|
949
960
|
listeners.forEach((listener) => listener(next));
|
|
950
961
|
}
|
|
951
962
|
function normalizeConversations(items) {
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
963
|
+
return [...items].sort((a, b) => a.sortAt < b.sortAt ? 1 : a.sortAt > b.sortAt ? -1 : 0);
|
|
964
|
+
}
|
|
965
|
+
function replaceConversations(next) {
|
|
966
|
+
const summaries = new Map(conversationSummaries);
|
|
967
|
+
next.forEach((item) => {
|
|
968
|
+
const id = item.conversation.id;
|
|
969
|
+
if (summaries.get(id)?.unreadCount !== item.unreadCount) {
|
|
970
|
+
unreadRevisions.set(id, (unreadRevisions.get(id) ?? 0) + 1);
|
|
971
|
+
}
|
|
972
|
+
summaries.set(id, item);
|
|
973
|
+
});
|
|
974
|
+
conversationSummaries = summaries;
|
|
975
|
+
conversations = next;
|
|
976
|
+
}
|
|
977
|
+
function replaceOneSummary(id, update) {
|
|
978
|
+
const held = conversationSummaries.get(id);
|
|
979
|
+
if (held === void 0) return;
|
|
980
|
+
const replacement = update(held);
|
|
981
|
+
if (held.unreadCount !== replacement.unreadCount) {
|
|
982
|
+
unreadRevisions.set(id, (unreadRevisions.get(id) ?? 0) + 1);
|
|
983
|
+
}
|
|
984
|
+
const summaries = new Map(conversationSummaries);
|
|
985
|
+
summaries.set(id, replacement);
|
|
986
|
+
conversationSummaries = summaries;
|
|
987
|
+
conversations = conversations.map(
|
|
988
|
+
(item) => item.conversation.id === id ? replacement : item
|
|
955
989
|
);
|
|
956
|
-
return [...zeroed].sort((a, b) => a.sortAt < b.sortAt ? 1 : a.sortAt > b.sortAt ? -1 : 0);
|
|
957
990
|
}
|
|
958
991
|
function threadFor(id) {
|
|
959
992
|
return threads.get(id) ?? {
|
|
@@ -965,7 +998,9 @@ function createMessagingStore() {
|
|
|
965
998
|
}
|
|
966
999
|
function writeThread(thread) {
|
|
967
1000
|
const next = new Map(threads);
|
|
968
|
-
const latest = thread.messages.
|
|
1001
|
+
const latest = [...thread.messages].reverse().find(
|
|
1002
|
+
(message) => message.deliveryState !== "sending" && message.deliveryState !== "failed" && !message.id.startsWith("optimistic:")
|
|
1003
|
+
);
|
|
969
1004
|
next.set(thread.conversationId, {
|
|
970
1005
|
...thread,
|
|
971
1006
|
latestAt: latest?.createdAt ?? thread.latestAt
|
|
@@ -981,7 +1016,7 @@ function createMessagingStore() {
|
|
|
981
1016
|
};
|
|
982
1017
|
},
|
|
983
1018
|
setConversations(items, hasMore) {
|
|
984
|
-
|
|
1019
|
+
replaceConversations(normalizeConversations(items));
|
|
985
1020
|
hasMoreConversations = hasMore;
|
|
986
1021
|
hasLoadedConversations = true;
|
|
987
1022
|
conversationsLoadError = null;
|
|
@@ -995,7 +1030,7 @@ function createMessagingStore() {
|
|
|
995
1030
|
setArchiveFilter(next) {
|
|
996
1031
|
if (next === archiveFilter) return;
|
|
997
1032
|
archiveFilter = next;
|
|
998
|
-
|
|
1033
|
+
replaceConversations([]);
|
|
999
1034
|
hasMoreConversations = false;
|
|
1000
1035
|
hasLoadedConversations = false;
|
|
1001
1036
|
emit();
|
|
@@ -1007,18 +1042,34 @@ function createMessagingStore() {
|
|
|
1007
1042
|
appendConversations(items, hasMore) {
|
|
1008
1043
|
const byId = new Map(conversations.map((item) => [item.conversation.id, item]));
|
|
1009
1044
|
items.forEach((item) => byId.set(item.conversation.id, item));
|
|
1010
|
-
|
|
1045
|
+
replaceConversations(normalizeConversations([...byId.values()]));
|
|
1011
1046
|
hasMoreConversations = hasMore;
|
|
1012
1047
|
emit();
|
|
1013
1048
|
},
|
|
1014
1049
|
upsertConversation(item) {
|
|
1015
1050
|
const byId = new Map(conversations.map((entry) => [entry.conversation.id, entry]));
|
|
1016
1051
|
byId.set(item.conversation.id, item);
|
|
1017
|
-
|
|
1052
|
+
replaceConversations(normalizeConversations([...byId.values()]));
|
|
1053
|
+
emit();
|
|
1054
|
+
},
|
|
1055
|
+
setConversationSummary(item) {
|
|
1056
|
+
const id = item.conversation.id;
|
|
1057
|
+
const held = conversationSummaries.get(id);
|
|
1058
|
+
if (held?.unreadCount !== item.unreadCount) {
|
|
1059
|
+
unreadRevisions.set(id, (unreadRevisions.get(id) ?? 0) + 1);
|
|
1060
|
+
}
|
|
1061
|
+
const summaries = new Map(conversationSummaries);
|
|
1062
|
+
summaries.set(id, item);
|
|
1063
|
+
conversationSummaries = summaries;
|
|
1018
1064
|
emit();
|
|
1019
1065
|
},
|
|
1020
1066
|
removeConversation(id) {
|
|
1021
|
-
conversations
|
|
1067
|
+
replaceConversations(conversations.filter((item) => item.conversation.id !== id));
|
|
1068
|
+
const summaries = new Map(conversationSummaries);
|
|
1069
|
+
summaries.delete(id);
|
|
1070
|
+
conversationSummaries = summaries;
|
|
1071
|
+
unreadRevisions.delete(id);
|
|
1072
|
+
confirmedReadRevisions.delete(id);
|
|
1022
1073
|
const next = new Map(threads);
|
|
1023
1074
|
next.delete(id);
|
|
1024
1075
|
threads = next;
|
|
@@ -1026,7 +1077,6 @@ function createMessagingStore() {
|
|
|
1026
1077
|
},
|
|
1027
1078
|
setActiveConversation(id) {
|
|
1028
1079
|
activeConversationId = id;
|
|
1029
|
-
conversations = normalizeConversations(conversations);
|
|
1030
1080
|
emit();
|
|
1031
1081
|
},
|
|
1032
1082
|
setConversationReadFailure(id, failure) {
|
|
@@ -1046,6 +1096,13 @@ function createMessagingStore() {
|
|
|
1046
1096
|
});
|
|
1047
1097
|
emit();
|
|
1048
1098
|
},
|
|
1099
|
+
clearThread(id) {
|
|
1100
|
+
if (!threads.has(id)) return;
|
|
1101
|
+
const next = new Map(threads);
|
|
1102
|
+
next.delete(id);
|
|
1103
|
+
threads = next;
|
|
1104
|
+
emit();
|
|
1105
|
+
},
|
|
1049
1106
|
prependOlder(id, messages, hasMoreOlder) {
|
|
1050
1107
|
const thread = threadFor(id);
|
|
1051
1108
|
const known = new Set(thread.messages.map((message) => message.id));
|
|
@@ -1098,40 +1155,48 @@ function createMessagingStore() {
|
|
|
1098
1155
|
emit();
|
|
1099
1156
|
},
|
|
1100
1157
|
applyLastMessage(message, args = {}) {
|
|
1101
|
-
const existing =
|
|
1102
|
-
(item) => item.conversation.id === message.conversationId
|
|
1103
|
-
);
|
|
1158
|
+
const existing = conversationSummaries.get(message.conversationId);
|
|
1104
1159
|
if (existing === void 0) return;
|
|
1105
1160
|
if (message.deliveryState === "sending" || message.deletedAt !== null) return;
|
|
1106
1161
|
if (existing.lastMessageAt !== null && message.createdAt < existing.lastMessageAt) return;
|
|
1107
|
-
const
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
unreadCount: shouldCount ? item.unreadCount + 1 : item.unreadCount
|
|
1118
|
-
} : item
|
|
1119
|
-
)
|
|
1120
|
-
);
|
|
1162
|
+
const shouldCount = args.incrementUnread === true;
|
|
1163
|
+
replaceOneSummary(message.conversationId, (item) => ({
|
|
1164
|
+
...item,
|
|
1165
|
+
lastMessageContent: message.content,
|
|
1166
|
+
lastMessageSenderId: message.senderId,
|
|
1167
|
+
lastMessageAt: message.createdAt,
|
|
1168
|
+
sortAt: message.createdAt,
|
|
1169
|
+
unreadCount: shouldCount ? item.unreadCount + 1 : item.unreadCount
|
|
1170
|
+
}));
|
|
1171
|
+
conversations = normalizeConversations(conversations);
|
|
1121
1172
|
emit();
|
|
1122
1173
|
},
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1174
|
+
captureConversationRead(id, at) {
|
|
1175
|
+
const item = conversationSummaries.get(id);
|
|
1176
|
+
return {
|
|
1177
|
+
at,
|
|
1178
|
+
unreadCount: item?.unreadCount ?? 0,
|
|
1179
|
+
revision: unreadRevisions.get(id) ?? 0
|
|
1180
|
+
};
|
|
1181
|
+
},
|
|
1182
|
+
markConversationRead(id, cutoff, canonicalUnreadCount = 0) {
|
|
1183
|
+
if (cutoff.revision <= (confirmedReadRevisions.get(id) ?? -1)) return;
|
|
1184
|
+
confirmedReadRevisions.set(id, cutoff.revision);
|
|
1185
|
+
replaceOneSummary(id, (item) => ({
|
|
1186
|
+
...item,
|
|
1187
|
+
// The repository re-counts after persisting the exact server cutoff.
|
|
1188
|
+
// If realtime advanced local state while that response was in flight,
|
|
1189
|
+
// retain at least the arrivals beyond the captured baseline.
|
|
1190
|
+
unreadCount: Math.max(
|
|
1191
|
+
0,
|
|
1192
|
+
canonicalUnreadCount,
|
|
1193
|
+
item.unreadCount - cutoff.unreadCount
|
|
1194
|
+
)
|
|
1195
|
+
}));
|
|
1127
1196
|
emit();
|
|
1128
1197
|
},
|
|
1129
1198
|
setUnreadCount(id, count) {
|
|
1130
|
-
|
|
1131
|
-
conversations.map(
|
|
1132
|
-
(item) => item.conversation.id === id ? { ...item, unreadCount: Math.max(0, count) } : item
|
|
1133
|
-
)
|
|
1134
|
-
);
|
|
1199
|
+
replaceOneSummary(id, (item) => ({ ...item, unreadCount: Math.max(0, count) }));
|
|
1135
1200
|
emit();
|
|
1136
1201
|
}
|
|
1137
1202
|
};
|
|
@@ -1168,21 +1233,57 @@ function createMessagingEngine(options) {
|
|
|
1168
1233
|
const conversationPageSize = options.conversationPageSize ?? 30;
|
|
1169
1234
|
const messagePageSize = options.messagePageSize ?? 50;
|
|
1170
1235
|
const openChannels = /* @__PURE__ */ new Map();
|
|
1236
|
+
const channelReadiness = /* @__PURE__ */ new Map();
|
|
1237
|
+
const settleChannelReadiness = /* @__PURE__ */ new Map();
|
|
1238
|
+
const readReceiptTails = /* @__PURE__ */ new Map();
|
|
1239
|
+
const persistedReadAt = /* @__PURE__ */ new Map();
|
|
1240
|
+
const initialReadDepth = /* @__PURE__ */ new Map();
|
|
1241
|
+
const initialReadGeneration = /* @__PURE__ */ new Map();
|
|
1242
|
+
const initialReadChanges = /* @__PURE__ */ new Map();
|
|
1171
1243
|
let inboxChannel = null;
|
|
1172
1244
|
let conversationCursor = null;
|
|
1173
1245
|
let disposed = false;
|
|
1174
1246
|
function report(event) {
|
|
1175
1247
|
options.onDiagnostic?.(event);
|
|
1176
1248
|
}
|
|
1177
|
-
function reportError(error, operation) {
|
|
1249
|
+
function reportError(error, operation, context = {}) {
|
|
1178
1250
|
const normalized = normalizeMessagingError(error, operation);
|
|
1179
1251
|
report({
|
|
1180
1252
|
// A missing session is a NORMAL lifecycle moment, not a red error. This
|
|
1181
1253
|
// one line is the cure for "909 captured errors in 0.6s".
|
|
1182
1254
|
level: normalized.code === "session-unavailable" ? "warn" : "error",
|
|
1183
1255
|
message: normalized.message,
|
|
1184
|
-
remedy: normalized.remedy
|
|
1256
|
+
remedy: normalized.remedy,
|
|
1257
|
+
code: normalized.code,
|
|
1258
|
+
operation,
|
|
1259
|
+
isRetryable: normalized.isRetryable,
|
|
1260
|
+
error: normalized,
|
|
1261
|
+
...context
|
|
1185
1262
|
});
|
|
1263
|
+
return normalized;
|
|
1264
|
+
}
|
|
1265
|
+
function persistRead(id, isLive = () => true) {
|
|
1266
|
+
const run = async () => {
|
|
1267
|
+
if (!isLive()) return;
|
|
1268
|
+
const at = store.snapshot().threads.get(id)?.latestAt ?? null;
|
|
1269
|
+
if (at === null || (persistedReadAt.get(id) ?? "") >= at) return;
|
|
1270
|
+
const cutoff = store.captureConversationRead(id, at);
|
|
1271
|
+
try {
|
|
1272
|
+
const canonicalUnreadCount = await repository.markRead(id, at);
|
|
1273
|
+
if (!isLive()) return;
|
|
1274
|
+
persistedReadAt.set(id, at);
|
|
1275
|
+
store.markConversationRead(id, cutoff, canonicalUnreadCount);
|
|
1276
|
+
} catch (error) {
|
|
1277
|
+
throw reportError(error, "markRead", { conversationId: id });
|
|
1278
|
+
}
|
|
1279
|
+
};
|
|
1280
|
+
const previous = readReceiptTails.get(id);
|
|
1281
|
+
const current = previous === void 0 ? run() : previous.catch(() => void 0).then(run);
|
|
1282
|
+
readReceiptTails.set(id, current);
|
|
1283
|
+
void current.finally(() => {
|
|
1284
|
+
if (readReceiptTails.get(id) === current) readReceiptTails.delete(id);
|
|
1285
|
+
}).catch(() => void 0);
|
|
1286
|
+
return current;
|
|
1186
1287
|
}
|
|
1187
1288
|
const outbox = createOutbox({
|
|
1188
1289
|
...options.outboxStorage !== void 0 ? { storage: options.outboxStorage } : {},
|
|
@@ -1267,6 +1368,137 @@ function createMessagingEngine(options) {
|
|
|
1267
1368
|
reportError(error, "backfillConversation");
|
|
1268
1369
|
}
|
|
1269
1370
|
}
|
|
1371
|
+
function applyLiveMessageChange(id, change) {
|
|
1372
|
+
if ((initialReadDepth.get(id) ?? 0) > 0) {
|
|
1373
|
+
const changes = initialReadChanges.get(id) ?? [];
|
|
1374
|
+
changes.push(change);
|
|
1375
|
+
initialReadChanges.set(id, changes);
|
|
1376
|
+
}
|
|
1377
|
+
if (change.kind === "delete") {
|
|
1378
|
+
store.removeMessage(id, change.messageId);
|
|
1379
|
+
} else {
|
|
1380
|
+
store.ingest(change.message);
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
function beginInitialRead(id) {
|
|
1384
|
+
const generation = (initialReadGeneration.get(id) ?? 0) + 1;
|
|
1385
|
+
initialReadGeneration.set(id, generation);
|
|
1386
|
+
initialReadDepth.set(id, (initialReadDepth.get(id) ?? 0) + 1);
|
|
1387
|
+
initialReadChanges.set(id, initialReadChanges.get(id) ?? []);
|
|
1388
|
+
return generation;
|
|
1389
|
+
}
|
|
1390
|
+
function finishInitialRead(id) {
|
|
1391
|
+
const depth = (initialReadDepth.get(id) ?? 1) - 1;
|
|
1392
|
+
if (depth > 0) {
|
|
1393
|
+
initialReadDepth.set(id, depth);
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
initialReadDepth.delete(id);
|
|
1397
|
+
initialReadChanges.delete(id);
|
|
1398
|
+
}
|
|
1399
|
+
function openIsLive(id, generation, channel) {
|
|
1400
|
+
return !disposed && initialReadGeneration.get(id) === generation && channel !== null && openChannels.get(id) === channel;
|
|
1401
|
+
}
|
|
1402
|
+
function installInitialPage(id, generation, page) {
|
|
1403
|
+
if (initialReadGeneration.get(id) !== generation) return false;
|
|
1404
|
+
const optimistic = (store.snapshot().threads.get(id)?.messages ?? []).filter(
|
|
1405
|
+
(item) => item.deliveryState === "sending" || item.deliveryState === "failed"
|
|
1406
|
+
);
|
|
1407
|
+
const confirmedClientKeys = new Set(
|
|
1408
|
+
page.items.flatMap(
|
|
1409
|
+
(item) => item.clientMessageId === null ? [] : [item.clientMessageId]
|
|
1410
|
+
)
|
|
1411
|
+
);
|
|
1412
|
+
store.setThread(id, page.items, { hasMoreOlder: page.hasMore });
|
|
1413
|
+
for (const change of initialReadChanges.get(id) ?? []) {
|
|
1414
|
+
if (change.kind === "delete") {
|
|
1415
|
+
store.removeMessage(id, change.messageId);
|
|
1416
|
+
} else {
|
|
1417
|
+
store.ingest(change.message);
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
store.ingestMany(
|
|
1421
|
+
optimistic.filter(
|
|
1422
|
+
(item) => item.clientMessageId === null || !confirmedClientKeys.has(item.clientMessageId)
|
|
1423
|
+
)
|
|
1424
|
+
);
|
|
1425
|
+
return true;
|
|
1426
|
+
}
|
|
1427
|
+
function ensureConversationChannel(id) {
|
|
1428
|
+
const existing = openChannels.get(id);
|
|
1429
|
+
if (existing !== void 0) {
|
|
1430
|
+
return channelReadiness.get(id) ?? Promise.resolve(existing.status() === "connected");
|
|
1431
|
+
}
|
|
1432
|
+
let markReady;
|
|
1433
|
+
const ready = new Promise((resolve) => {
|
|
1434
|
+
markReady = resolve;
|
|
1435
|
+
});
|
|
1436
|
+
const handle = manager.open({
|
|
1437
|
+
topic: conversationTopic(id),
|
|
1438
|
+
// Presence callbacks are JOIN-TIME in supabase-js. Declare the read-only
|
|
1439
|
+
// room shape on the first holder so later holders do not create a gap.
|
|
1440
|
+
presence: { publish: false, state: {} },
|
|
1441
|
+
postgresChanges: [
|
|
1442
|
+
{
|
|
1443
|
+
event: "*",
|
|
1444
|
+
schema: "communication",
|
|
1445
|
+
table: "dm_messages",
|
|
1446
|
+
filter: `conversation_id=eq.${id}`,
|
|
1447
|
+
rowId: (row) => typeof row["id"] === "string" ? row["id"] : void 0,
|
|
1448
|
+
fingerprint: (row) => typeof row["content"] === "string" ? row["content"] : void 0,
|
|
1449
|
+
updatedAtField: "updated_at",
|
|
1450
|
+
updatedByField: "updated_by",
|
|
1451
|
+
onChange: ({ payload, row }) => {
|
|
1452
|
+
if (row === null) return;
|
|
1453
|
+
if (payload?.eventType === "DELETE" || typeof row["deleted_at"] === "string" && row["deleted_at"].length > 0) {
|
|
1454
|
+
const rawId = row["id"];
|
|
1455
|
+
if (typeof rawId !== "string" || rawId.length === 0) {
|
|
1456
|
+
reportError(
|
|
1457
|
+
new Error("A message deletion payload carried no message id."),
|
|
1458
|
+
"conversationMessageDelete",
|
|
1459
|
+
{ conversationId: id }
|
|
1460
|
+
);
|
|
1461
|
+
return;
|
|
1462
|
+
}
|
|
1463
|
+
applyLiveMessageChange(id, {
|
|
1464
|
+
kind: "delete",
|
|
1465
|
+
messageId: asMessageId(rawId)
|
|
1466
|
+
});
|
|
1467
|
+
return;
|
|
1468
|
+
}
|
|
1469
|
+
const message = projectMessage(row, identity.organizationId);
|
|
1470
|
+
applyLiveMessageChange(id, { kind: "upsert", message });
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
],
|
|
1474
|
+
broadcast: [
|
|
1475
|
+
{
|
|
1476
|
+
event: MESSAGING_EVENTS.message,
|
|
1477
|
+
onMessage: ({ data }) => {
|
|
1478
|
+
try {
|
|
1479
|
+
const message = projectBroadcastMessage(data);
|
|
1480
|
+
applyLiveMessageChange(id, { kind: "upsert", message });
|
|
1481
|
+
} catch (error) {
|
|
1482
|
+
reportError(error, "broadcastMessage");
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
],
|
|
1487
|
+
onStatusChange: (status) => {
|
|
1488
|
+
if (status === "connected") markReady?.(true);
|
|
1489
|
+
if (status === "disconnected") markReady?.(false);
|
|
1490
|
+
},
|
|
1491
|
+
onBackfill: () => {
|
|
1492
|
+
void backfillConversation(id);
|
|
1493
|
+
outbox.flush();
|
|
1494
|
+
}
|
|
1495
|
+
});
|
|
1496
|
+
openChannels.set(id, handle);
|
|
1497
|
+
channelReadiness.set(id, ready);
|
|
1498
|
+
settleChannelReadiness.set(id, (connected) => markReady?.(connected));
|
|
1499
|
+
if (handle.status() === "connected") markReady?.(true);
|
|
1500
|
+
return ready;
|
|
1501
|
+
}
|
|
1270
1502
|
const engine = {
|
|
1271
1503
|
store,
|
|
1272
1504
|
outbox,
|
|
@@ -1292,7 +1524,9 @@ function createMessagingEngine(options) {
|
|
|
1292
1524
|
onChange: ({ row }) => {
|
|
1293
1525
|
if (row === null) return;
|
|
1294
1526
|
const message = projectMessage(row, identity.organizationId);
|
|
1295
|
-
const known = store.snapshot().
|
|
1527
|
+
const known = store.snapshot().conversationSummaries.has(
|
|
1528
|
+
message.conversationId
|
|
1529
|
+
);
|
|
1296
1530
|
if (!known) {
|
|
1297
1531
|
void reloadInbox().catch((error) => reportError(error, "inboxRefresh"));
|
|
1298
1532
|
return;
|
|
@@ -1300,6 +1534,9 @@ function createMessagingEngine(options) {
|
|
|
1300
1534
|
store.ingest(message);
|
|
1301
1535
|
const isMine = message.senderId === identity.userId;
|
|
1302
1536
|
store.applyLastMessage(message, { incrementUnread: !isMine });
|
|
1537
|
+
if (!isMine && store.snapshot().activeConversationId === message.conversationId) {
|
|
1538
|
+
void persistRead(message.conversationId).catch(() => void 0);
|
|
1539
|
+
}
|
|
1303
1540
|
if (!isMine) options.onIncoming?.(message);
|
|
1304
1541
|
}
|
|
1305
1542
|
},
|
|
@@ -1364,18 +1601,35 @@ function createMessagingEngine(options) {
|
|
|
1364
1601
|
},
|
|
1365
1602
|
async openConversation(id) {
|
|
1366
1603
|
store.setActiveConversation(id);
|
|
1604
|
+
const generation = beginInitialRead(id);
|
|
1605
|
+
let ownedChannel = null;
|
|
1367
1606
|
try {
|
|
1607
|
+
const connected = await ensureConversationChannel(id);
|
|
1608
|
+
ownedChannel = openChannels.get(id) ?? null;
|
|
1609
|
+
if (!connected || !openIsLive(id, generation, ownedChannel)) return;
|
|
1368
1610
|
const page = await repository.listMessages(id, { limit: messagePageSize });
|
|
1369
|
-
if (
|
|
1370
|
-
|
|
1371
|
-
|
|
1611
|
+
if (!openIsLive(id, generation, ownedChannel)) return;
|
|
1612
|
+
if (!installInitialPage(id, generation, page)) return;
|
|
1613
|
+
const missingSummary = !store.snapshot().conversationSummaries.has(id);
|
|
1614
|
+
const directSummary = missingSummary ? await repository.getConversationSummary(id) : null;
|
|
1615
|
+
if (!openIsLive(id, generation, ownedChannel)) return;
|
|
1372
1616
|
store.setConversationReadFailure(id, null);
|
|
1373
|
-
|
|
1617
|
+
if (directSummary !== null) store.setConversationSummary(directSummary);
|
|
1618
|
+
await persistRead(
|
|
1619
|
+
id,
|
|
1620
|
+
() => openIsLive(id, generation, ownedChannel)
|
|
1621
|
+
).catch(() => void 0);
|
|
1374
1622
|
} catch (error) {
|
|
1623
|
+
if (!openIsLive(id, generation, ownedChannel)) return;
|
|
1375
1624
|
const normalized = normalizeMessagingError(error, "openConversation");
|
|
1376
1625
|
const unavailable = normalized.code === "not-found" || normalized.code === "forbidden";
|
|
1377
1626
|
store.setConversationReadFailure(id, { error: normalized, unavailable });
|
|
1378
1627
|
if (unavailable) {
|
|
1628
|
+
openChannels.get(id)?.close();
|
|
1629
|
+
openChannels.delete(id);
|
|
1630
|
+
settleChannelReadiness.delete(id);
|
|
1631
|
+
channelReadiness.delete(id);
|
|
1632
|
+
store.clearThread(id);
|
|
1379
1633
|
report({
|
|
1380
1634
|
level: "warn",
|
|
1381
1635
|
message: normalized.message,
|
|
@@ -1384,53 +1638,17 @@ function createMessagingEngine(options) {
|
|
|
1384
1638
|
return;
|
|
1385
1639
|
}
|
|
1386
1640
|
reportError(normalized, "openConversation");
|
|
1641
|
+
} finally {
|
|
1642
|
+
finishInitialRead(id);
|
|
1387
1643
|
}
|
|
1388
|
-
if (openChannels.has(id) || disposed) return;
|
|
1389
|
-
const handle = manager.open({
|
|
1390
|
-
topic: conversationTopic(id),
|
|
1391
|
-
// Presence callbacks are JOIN-TIME in supabase-js. Declare the
|
|
1392
|
-
// read-only room shape on the first holder (the engine) so the
|
|
1393
|
-
// composer/typist/online hooks can attach without forcing a rebuild
|
|
1394
|
-
// after this durable channel has already subscribed.
|
|
1395
|
-
presence: { publish: false, state: {} },
|
|
1396
|
-
postgresChanges: [
|
|
1397
|
-
{
|
|
1398
|
-
event: "*",
|
|
1399
|
-
schema: "communication",
|
|
1400
|
-
table: "dm_messages",
|
|
1401
|
-
filter: `conversation_id=eq.${id}`,
|
|
1402
|
-
rowId: (row) => typeof row["id"] === "string" ? row["id"] : void 0,
|
|
1403
|
-
fingerprint: (row) => typeof row["content"] === "string" ? row["content"] : void 0,
|
|
1404
|
-
updatedAtField: "updated_at",
|
|
1405
|
-
updatedByField: "updated_by",
|
|
1406
|
-
onChange: ({ row }) => {
|
|
1407
|
-
if (row === null) return;
|
|
1408
|
-
store.ingest(projectMessage(row, identity.organizationId));
|
|
1409
|
-
}
|
|
1410
|
-
}
|
|
1411
|
-
],
|
|
1412
|
-
broadcast: [
|
|
1413
|
-
{
|
|
1414
|
-
event: MESSAGING_EVENTS.message,
|
|
1415
|
-
onMessage: ({ data }) => {
|
|
1416
|
-
try {
|
|
1417
|
-
store.ingest(projectBroadcastMessage(data));
|
|
1418
|
-
} catch (error) {
|
|
1419
|
-
reportError(error, "broadcastMessage");
|
|
1420
|
-
}
|
|
1421
|
-
}
|
|
1422
|
-
}
|
|
1423
|
-
],
|
|
1424
|
-
onBackfill: () => {
|
|
1425
|
-
void backfillConversation(id);
|
|
1426
|
-
outbox.flush();
|
|
1427
|
-
}
|
|
1428
|
-
});
|
|
1429
|
-
openChannels.set(id, handle);
|
|
1430
1644
|
},
|
|
1431
1645
|
closeConversation(id) {
|
|
1646
|
+
initialReadGeneration.set(id, (initialReadGeneration.get(id) ?? 0) + 1);
|
|
1432
1647
|
openChannels.get(id)?.close();
|
|
1433
1648
|
openChannels.delete(id);
|
|
1649
|
+
settleChannelReadiness.get(id)?.(false);
|
|
1650
|
+
settleChannelReadiness.delete(id);
|
|
1651
|
+
channelReadiness.delete(id);
|
|
1434
1652
|
if (store.snapshot().activeConversationId === id) {
|
|
1435
1653
|
store.setActiveConversation(null);
|
|
1436
1654
|
}
|
|
@@ -1482,12 +1700,7 @@ function createMessagingEngine(options) {
|
|
|
1482
1700
|
}
|
|
1483
1701
|
},
|
|
1484
1702
|
async markRead(id) {
|
|
1485
|
-
|
|
1486
|
-
try {
|
|
1487
|
-
await repository.markRead(id);
|
|
1488
|
-
} catch (error) {
|
|
1489
|
-
reportError(error, "markRead");
|
|
1490
|
-
}
|
|
1703
|
+
await persistRead(id);
|
|
1491
1704
|
},
|
|
1492
1705
|
async startDirectConversation(otherUserId) {
|
|
1493
1706
|
const id = await repository.getOrCreateDirectConversation(otherUserId);
|
|
@@ -1496,9 +1709,15 @@ function createMessagingEngine(options) {
|
|
|
1496
1709
|
},
|
|
1497
1710
|
dispose() {
|
|
1498
1711
|
disposed = true;
|
|
1712
|
+
openChannels.forEach((_handle, id) => {
|
|
1713
|
+
initialReadGeneration.set(id, (initialReadGeneration.get(id) ?? 0) + 1);
|
|
1714
|
+
});
|
|
1499
1715
|
outbox.dispose();
|
|
1500
1716
|
openChannels.forEach((handle) => handle.close());
|
|
1501
1717
|
openChannels.clear();
|
|
1718
|
+
settleChannelReadiness.forEach((settle) => settle(false));
|
|
1719
|
+
settleChannelReadiness.clear();
|
|
1720
|
+
channelReadiness.clear();
|
|
1502
1721
|
inboxChannel?.close();
|
|
1503
1722
|
inboxChannel = null;
|
|
1504
1723
|
}
|
|
@@ -1712,6 +1931,7 @@ var RPCS = {
|
|
|
1712
1931
|
isParticipant: "is_dm_participant"
|
|
1713
1932
|
};
|
|
1714
1933
|
var MESSAGE_COLUMNS = "id,conversation_id,sender_id,organization_id,content,message_type,status,reply_to_id,client_message_id,action_data,media_url,media_thumbnail_url,media_metadata,created_at,edited_at,deleted_at,deleted_for_everyone,metadata";
|
|
1934
|
+
var PARTICIPANT_COLUMNS = "conversation_id,user_id,role,joined_at,last_read_at,is_muted,is_archived";
|
|
1715
1935
|
var CONVERSATION_COLUMNS = "id,type,group_name,group_image_url,created_by,organization_id,created_at,updated_at,metadata";
|
|
1716
1936
|
function optionalOrg(organizationId) {
|
|
1717
1937
|
return typeof organizationId === "string" && organizationId.length > 0 ? organizationId : null;
|
|
@@ -1781,6 +2001,14 @@ function createMessagingRepository(options) {
|
|
|
1781
2001
|
function actingUser(operation) {
|
|
1782
2002
|
return withSessionRetry(operation, () => actingUserId());
|
|
1783
2003
|
}
|
|
2004
|
+
async function updateLiveParticipant(operation, conversationId, userId, patch) {
|
|
2005
|
+
const destination = await organizationOfExisting(conversationId);
|
|
2006
|
+
const { data, error } = await db().from(TABLES.participants).update({ ...patch, organization_id: destination }).eq("conversation_id", conversationId).eq("user_id", userId).eq("organization_id", destination).is("deleted_at", null).select("id").maybeSingle();
|
|
2007
|
+
if (error !== null) throw normalizeMessagingError(error, operation);
|
|
2008
|
+
if (data === null) {
|
|
2009
|
+
throw writeNotApplied(operation, "participant update returned no live row");
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
1784
2012
|
async function rpc(fn, args, operation) {
|
|
1785
2013
|
const { data, error } = await rpcDb().rpc(fn, args);
|
|
1786
2014
|
if (error !== null) throw normalizeMessagingError(error, operation);
|
|
@@ -1803,6 +2031,33 @@ function createMessagingRepository(options) {
|
|
|
1803
2031
|
});
|
|
1804
2032
|
return found;
|
|
1805
2033
|
}
|
|
2034
|
+
async function allLiveParticipants(conversationId) {
|
|
2035
|
+
const operation = "getConversationSummary";
|
|
2036
|
+
const pageSize = 1e3;
|
|
2037
|
+
const rows = [];
|
|
2038
|
+
for (let from = 0; ; from += pageSize) {
|
|
2039
|
+
const response = await withSessionRetry(
|
|
2040
|
+
operation,
|
|
2041
|
+
async () => db().from(TABLES.participants).select(PARTICIPANT_COLUMNS, { count: "exact" }).eq("conversation_id", conversationId).is("deleted_at", null).order("user_id", { ascending: true }).range(from, from + pageSize - 1)
|
|
2042
|
+
);
|
|
2043
|
+
if (response.error !== null) {
|
|
2044
|
+
throw normalizeMessagingError(response.error, operation);
|
|
2045
|
+
}
|
|
2046
|
+
const total = response.count;
|
|
2047
|
+
if (typeof total !== "number" || !Number.isInteger(total) || total < 0) {
|
|
2048
|
+
throw invalidResponse(operation, "participant read returned no exact count");
|
|
2049
|
+
}
|
|
2050
|
+
const page = response.data ?? [];
|
|
2051
|
+
rows.push(...page);
|
|
2052
|
+
if (rows.length >= total) return rows;
|
|
2053
|
+
if (page.length === 0) {
|
|
2054
|
+
throw invalidResponse(
|
|
2055
|
+
operation,
|
|
2056
|
+
`participant read stopped at ${rows.length} of ${total}`
|
|
2057
|
+
);
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
1806
2061
|
const conversationOrganizations = /* @__PURE__ */ new Map();
|
|
1807
2062
|
async function organizationOfExisting(conversationId) {
|
|
1808
2063
|
const known = conversationOrganizations.get(conversationId);
|
|
@@ -1914,6 +2169,78 @@ function createMessagingRepository(options) {
|
|
|
1914
2169
|
);
|
|
1915
2170
|
return summary.conversation;
|
|
1916
2171
|
},
|
|
2172
|
+
async getConversationSummary(id) {
|
|
2173
|
+
const operation = "getConversationSummary";
|
|
2174
|
+
const me = await actingUser(operation);
|
|
2175
|
+
const [conversationResult, participantRows, latestResult, unreadRaw] = await withSessionRetry(
|
|
2176
|
+
operation,
|
|
2177
|
+
() => Promise.all([
|
|
2178
|
+
db().from(TABLES.conversations).select(CONVERSATION_COLUMNS).eq("id", id).single(),
|
|
2179
|
+
allLiveParticipants(id),
|
|
2180
|
+
db().from(TABLES.messages).select(MESSAGE_COLUMNS).eq("conversation_id", id).is("deleted_at", null).order("created_at", { ascending: false }).order("id", { ascending: false }).limit(1).maybeSingle(),
|
|
2181
|
+
rpc(
|
|
2182
|
+
RPCS.unreadCount,
|
|
2183
|
+
{ p_conversation_id: id, p_user_id: me },
|
|
2184
|
+
operation
|
|
2185
|
+
)
|
|
2186
|
+
])
|
|
2187
|
+
);
|
|
2188
|
+
if (conversationResult.error !== null) {
|
|
2189
|
+
throw normalizeMessagingError(conversationResult.error, operation);
|
|
2190
|
+
}
|
|
2191
|
+
if (latestResult.error !== null) {
|
|
2192
|
+
throw normalizeMessagingError(latestResult.error, operation);
|
|
2193
|
+
}
|
|
2194
|
+
const conversation = conversationResult.data;
|
|
2195
|
+
if (conversation === null) {
|
|
2196
|
+
throw invalidResponse(operation, `conversation ${id} returned no row`);
|
|
2197
|
+
}
|
|
2198
|
+
if (typeof unreadRaw !== "number" || !Number.isFinite(unreadRaw) || unreadRaw < 0) {
|
|
2199
|
+
throw invalidResponse(operation, `${RPCS.unreadCount} returned ${JSON.stringify(unreadRaw)}`);
|
|
2200
|
+
}
|
|
2201
|
+
const participantIds = participantRows.flatMap((row) => {
|
|
2202
|
+
const value = row["user_id"];
|
|
2203
|
+
return typeof value === "string" && value.length > 0 ? [projectUserSummary({ user_id: value }).userId] : [];
|
|
2204
|
+
});
|
|
2205
|
+
const profiles = await getUsers(participantIds);
|
|
2206
|
+
const participants = participantIds.map((userId) => {
|
|
2207
|
+
const profile = profiles.get(userId);
|
|
2208
|
+
return profile === void 0 ? {
|
|
2209
|
+
user_id: userId,
|
|
2210
|
+
display_name: userId
|
|
2211
|
+
} : {
|
|
2212
|
+
user_id: profile.userId,
|
|
2213
|
+
display_name: profile.displayName,
|
|
2214
|
+
email: profile.email,
|
|
2215
|
+
avatar_url: profile.avatarUrl,
|
|
2216
|
+
is_agent: profile.isAgent
|
|
2217
|
+
};
|
|
2218
|
+
});
|
|
2219
|
+
const viewer = participantRows.find((row) => row["user_id"] === me);
|
|
2220
|
+
const latest = latestResult.data;
|
|
2221
|
+
return projectConversationSummary(
|
|
2222
|
+
{
|
|
2223
|
+
conversation_id: conversation["id"],
|
|
2224
|
+
conversation_type: conversation["type"],
|
|
2225
|
+
group_name: conversation["group_name"],
|
|
2226
|
+
group_image_url: conversation["group_image_url"],
|
|
2227
|
+
conversation_created_at: conversation["created_at"],
|
|
2228
|
+
conversation_updated_at: conversation["updated_at"],
|
|
2229
|
+
organization_id: conversation["organization_id"],
|
|
2230
|
+
created_by: conversation["created_by"],
|
|
2231
|
+
metadata: conversation["metadata"],
|
|
2232
|
+
participants,
|
|
2233
|
+
last_message_content: latest?.["content"] ?? null,
|
|
2234
|
+
last_message_sender_id: latest?.["sender_id"] ?? null,
|
|
2235
|
+
last_message_at: latest?.["created_at"] ?? null,
|
|
2236
|
+
unread_count: Math.floor(unreadRaw),
|
|
2237
|
+
is_muted: viewer?.["is_muted"] ?? false,
|
|
2238
|
+
is_archived: viewer?.["is_archived"] ?? false
|
|
2239
|
+
},
|
|
2240
|
+
me,
|
|
2241
|
+
org
|
|
2242
|
+
);
|
|
2243
|
+
},
|
|
1917
2244
|
async listMessages(conversationId, args = {}) {
|
|
1918
2245
|
const limit = args.limit ?? 50;
|
|
1919
2246
|
const operation = "listMessages";
|
|
@@ -2059,8 +2386,22 @@ function createMessagingRepository(options) {
|
|
|
2059
2386
|
async markRead(conversationId, at) {
|
|
2060
2387
|
const operation = "markRead";
|
|
2061
2388
|
const me = await actingUser(operation);
|
|
2062
|
-
|
|
2063
|
-
|
|
2389
|
+
await updateLiveParticipant(operation, conversationId, me, {
|
|
2390
|
+
last_read_at: at,
|
|
2391
|
+
updated_by: me
|
|
2392
|
+
});
|
|
2393
|
+
const unread = await withSessionRetry(
|
|
2394
|
+
operation,
|
|
2395
|
+
() => rpc(
|
|
2396
|
+
RPCS.unreadCount,
|
|
2397
|
+
{ p_conversation_id: conversationId, p_user_id: me },
|
|
2398
|
+
operation
|
|
2399
|
+
)
|
|
2400
|
+
);
|
|
2401
|
+
if (typeof unread !== "number" || !Number.isFinite(unread) || unread < 0) {
|
|
2402
|
+
throw invalidResponse(operation, `${RPCS.unreadCount} returned ${JSON.stringify(unread)}`);
|
|
2403
|
+
}
|
|
2404
|
+
return Math.floor(unread);
|
|
2064
2405
|
},
|
|
2065
2406
|
async setConversationFlags(conversationId, flags) {
|
|
2066
2407
|
const operation = "setConversationFlags";
|
|
@@ -2068,8 +2409,7 @@ function createMessagingRepository(options) {
|
|
|
2068
2409
|
const patch = { updated_by: me };
|
|
2069
2410
|
if (flags.isMuted !== void 0) patch["is_muted"] = flags.isMuted;
|
|
2070
2411
|
if (flags.isArchived !== void 0) patch["is_archived"] = flags.isArchived;
|
|
2071
|
-
|
|
2072
|
-
if (error !== null) throw normalizeMessagingError(error, operation);
|
|
2412
|
+
await updateLiveParticipant(operation, conversationId, me, patch);
|
|
2073
2413
|
},
|
|
2074
2414
|
async addMembers(conversationId, memberIds) {
|
|
2075
2415
|
const operation = "addMembers";
|
|
@@ -2089,14 +2429,18 @@ function createMessagingRepository(options) {
|
|
|
2089
2429
|
async removeMember(conversationId, memberId) {
|
|
2090
2430
|
const operation = "removeMember";
|
|
2091
2431
|
const me = await actingUser(operation);
|
|
2092
|
-
|
|
2093
|
-
|
|
2432
|
+
await updateLiveParticipant(operation, conversationId, memberId, {
|
|
2433
|
+
deleted_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2434
|
+
updated_by: me
|
|
2435
|
+
});
|
|
2094
2436
|
},
|
|
2095
2437
|
async setMemberRole(conversationId, memberId, role) {
|
|
2096
2438
|
const operation = "setMemberRole";
|
|
2097
2439
|
const me = await actingUser(operation);
|
|
2098
|
-
|
|
2099
|
-
|
|
2440
|
+
await updateLiveParticipant(operation, conversationId, memberId, {
|
|
2441
|
+
role,
|
|
2442
|
+
updated_by: me
|
|
2443
|
+
});
|
|
2100
2444
|
},
|
|
2101
2445
|
getUser(userId) {
|
|
2102
2446
|
const operation = "getUser";
|
|
@@ -2481,7 +2825,6 @@ function useConversations() {
|
|
|
2481
2825
|
const select = useCallback(
|
|
2482
2826
|
(id) => {
|
|
2483
2827
|
void host?.engine.openConversation(id);
|
|
2484
|
-
void host?.engine.markRead(id);
|
|
2485
2828
|
},
|
|
2486
2829
|
[host]
|
|
2487
2830
|
);
|
|
@@ -2532,7 +2875,7 @@ function useConversation(id) {
|
|
|
2532
2875
|
return () => clearInterval(timer);
|
|
2533
2876
|
}, []);
|
|
2534
2877
|
const thread = id === null ? null : snapshot?.threads.get(id) ?? null;
|
|
2535
|
-
const summary = id === null ? null : snapshot?.
|
|
2878
|
+
const summary = id === null ? null : snapshot?.conversationSummaries.get(id) ?? null;
|
|
2536
2879
|
const failure = id === null ? null : snapshot?.conversationReadFailures.get(id) ?? null;
|
|
2537
2880
|
const pending = useMemo2(() => {
|
|
2538
2881
|
void outboxTick;
|
|
@@ -4043,6 +4386,7 @@ export {
|
|
|
4043
4386
|
useMessagingSnapshot,
|
|
4044
4387
|
useOnlineUserIds,
|
|
4045
4388
|
useRequiredMessagingHost,
|
|
4046
|
-
useTypists
|
|
4389
|
+
useTypists,
|
|
4390
|
+
writeNotApplied
|
|
4047
4391
|
};
|
|
4048
4392
|
//# sourceMappingURL=react.js.map
|