@ai-matrx/messaging 0.15.14 → 0.15.16
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 +4 -4
package/dist/index.js
CHANGED
|
@@ -235,6 +235,13 @@ function invalidResponse(operation, detail) {
|
|
|
235
235
|
"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."
|
|
236
236
|
);
|
|
237
237
|
}
|
|
238
|
+
function writeNotApplied(operation, detail) {
|
|
239
|
+
return new MessagingError(
|
|
240
|
+
"write-not-applied",
|
|
241
|
+
`${operation}: ${detail}`,
|
|
242
|
+
"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."
|
|
243
|
+
);
|
|
244
|
+
}
|
|
238
245
|
|
|
239
246
|
// src/core/references.ts
|
|
240
247
|
var FENCE = /```matrx\s*\n([\s\S]*?)\n?```/g;
|
|
@@ -1016,6 +1023,7 @@ function insertOrdered(messages, message) {
|
|
|
1016
1023
|
}
|
|
1017
1024
|
function createMessagingStore() {
|
|
1018
1025
|
let conversations = [];
|
|
1026
|
+
let conversationSummaries = /* @__PURE__ */ new Map();
|
|
1019
1027
|
let hasMoreConversations = false;
|
|
1020
1028
|
let hasLoadedConversations = false;
|
|
1021
1029
|
let archiveFilter = DEFAULT_MESSAGING_ARCHIVE_FILTER;
|
|
@@ -1024,12 +1032,15 @@ function createMessagingStore() {
|
|
|
1024
1032
|
let activeConversationId = null;
|
|
1025
1033
|
let conversationReadFailures = /* @__PURE__ */ new Map();
|
|
1026
1034
|
let conversationsLoadError = null;
|
|
1035
|
+
const unreadRevisions = /* @__PURE__ */ new Map();
|
|
1036
|
+
const confirmedReadRevisions = /* @__PURE__ */ new Map();
|
|
1027
1037
|
const listeners = /* @__PURE__ */ new Set();
|
|
1028
1038
|
let cached = null;
|
|
1029
1039
|
function snapshot() {
|
|
1030
1040
|
if (cached !== null) return cached;
|
|
1031
1041
|
cached = {
|
|
1032
1042
|
conversations,
|
|
1043
|
+
conversationSummaries,
|
|
1033
1044
|
hasMoreConversations,
|
|
1034
1045
|
hasLoadedConversations,
|
|
1035
1046
|
threads,
|
|
@@ -1048,11 +1059,33 @@ function createMessagingStore() {
|
|
|
1048
1059
|
listeners.forEach((listener) => listener(next));
|
|
1049
1060
|
}
|
|
1050
1061
|
function normalizeConversations(items) {
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1062
|
+
return [...items].sort((a, b) => a.sortAt < b.sortAt ? 1 : a.sortAt > b.sortAt ? -1 : 0);
|
|
1063
|
+
}
|
|
1064
|
+
function replaceConversations(next) {
|
|
1065
|
+
const summaries = new Map(conversationSummaries);
|
|
1066
|
+
next.forEach((item) => {
|
|
1067
|
+
const id = item.conversation.id;
|
|
1068
|
+
if (summaries.get(id)?.unreadCount !== item.unreadCount) {
|
|
1069
|
+
unreadRevisions.set(id, (unreadRevisions.get(id) ?? 0) + 1);
|
|
1070
|
+
}
|
|
1071
|
+
summaries.set(id, item);
|
|
1072
|
+
});
|
|
1073
|
+
conversationSummaries = summaries;
|
|
1074
|
+
conversations = next;
|
|
1075
|
+
}
|
|
1076
|
+
function replaceOneSummary(id, update) {
|
|
1077
|
+
const held = conversationSummaries.get(id);
|
|
1078
|
+
if (held === void 0) return;
|
|
1079
|
+
const replacement = update(held);
|
|
1080
|
+
if (held.unreadCount !== replacement.unreadCount) {
|
|
1081
|
+
unreadRevisions.set(id, (unreadRevisions.get(id) ?? 0) + 1);
|
|
1082
|
+
}
|
|
1083
|
+
const summaries = new Map(conversationSummaries);
|
|
1084
|
+
summaries.set(id, replacement);
|
|
1085
|
+
conversationSummaries = summaries;
|
|
1086
|
+
conversations = conversations.map(
|
|
1087
|
+
(item) => item.conversation.id === id ? replacement : item
|
|
1054
1088
|
);
|
|
1055
|
-
return [...zeroed].sort((a, b) => a.sortAt < b.sortAt ? 1 : a.sortAt > b.sortAt ? -1 : 0);
|
|
1056
1089
|
}
|
|
1057
1090
|
function threadFor(id) {
|
|
1058
1091
|
return threads.get(id) ?? {
|
|
@@ -1064,7 +1097,9 @@ function createMessagingStore() {
|
|
|
1064
1097
|
}
|
|
1065
1098
|
function writeThread(thread) {
|
|
1066
1099
|
const next = new Map(threads);
|
|
1067
|
-
const latest = thread.messages.
|
|
1100
|
+
const latest = [...thread.messages].reverse().find(
|
|
1101
|
+
(message) => message.deliveryState !== "sending" && message.deliveryState !== "failed" && !message.id.startsWith("optimistic:")
|
|
1102
|
+
);
|
|
1068
1103
|
next.set(thread.conversationId, {
|
|
1069
1104
|
...thread,
|
|
1070
1105
|
latestAt: latest?.createdAt ?? thread.latestAt
|
|
@@ -1080,7 +1115,7 @@ function createMessagingStore() {
|
|
|
1080
1115
|
};
|
|
1081
1116
|
},
|
|
1082
1117
|
setConversations(items, hasMore) {
|
|
1083
|
-
|
|
1118
|
+
replaceConversations(normalizeConversations(items));
|
|
1084
1119
|
hasMoreConversations = hasMore;
|
|
1085
1120
|
hasLoadedConversations = true;
|
|
1086
1121
|
conversationsLoadError = null;
|
|
@@ -1094,7 +1129,7 @@ function createMessagingStore() {
|
|
|
1094
1129
|
setArchiveFilter(next) {
|
|
1095
1130
|
if (next === archiveFilter) return;
|
|
1096
1131
|
archiveFilter = next;
|
|
1097
|
-
|
|
1132
|
+
replaceConversations([]);
|
|
1098
1133
|
hasMoreConversations = false;
|
|
1099
1134
|
hasLoadedConversations = false;
|
|
1100
1135
|
emit();
|
|
@@ -1106,18 +1141,34 @@ function createMessagingStore() {
|
|
|
1106
1141
|
appendConversations(items, hasMore) {
|
|
1107
1142
|
const byId = new Map(conversations.map((item) => [item.conversation.id, item]));
|
|
1108
1143
|
items.forEach((item) => byId.set(item.conversation.id, item));
|
|
1109
|
-
|
|
1144
|
+
replaceConversations(normalizeConversations([...byId.values()]));
|
|
1110
1145
|
hasMoreConversations = hasMore;
|
|
1111
1146
|
emit();
|
|
1112
1147
|
},
|
|
1113
1148
|
upsertConversation(item) {
|
|
1114
1149
|
const byId = new Map(conversations.map((entry) => [entry.conversation.id, entry]));
|
|
1115
1150
|
byId.set(item.conversation.id, item);
|
|
1116
|
-
|
|
1151
|
+
replaceConversations(normalizeConversations([...byId.values()]));
|
|
1152
|
+
emit();
|
|
1153
|
+
},
|
|
1154
|
+
setConversationSummary(item) {
|
|
1155
|
+
const id = item.conversation.id;
|
|
1156
|
+
const held = conversationSummaries.get(id);
|
|
1157
|
+
if (held?.unreadCount !== item.unreadCount) {
|
|
1158
|
+
unreadRevisions.set(id, (unreadRevisions.get(id) ?? 0) + 1);
|
|
1159
|
+
}
|
|
1160
|
+
const summaries = new Map(conversationSummaries);
|
|
1161
|
+
summaries.set(id, item);
|
|
1162
|
+
conversationSummaries = summaries;
|
|
1117
1163
|
emit();
|
|
1118
1164
|
},
|
|
1119
1165
|
removeConversation(id) {
|
|
1120
|
-
conversations
|
|
1166
|
+
replaceConversations(conversations.filter((item) => item.conversation.id !== id));
|
|
1167
|
+
const summaries = new Map(conversationSummaries);
|
|
1168
|
+
summaries.delete(id);
|
|
1169
|
+
conversationSummaries = summaries;
|
|
1170
|
+
unreadRevisions.delete(id);
|
|
1171
|
+
confirmedReadRevisions.delete(id);
|
|
1121
1172
|
const next = new Map(threads);
|
|
1122
1173
|
next.delete(id);
|
|
1123
1174
|
threads = next;
|
|
@@ -1125,7 +1176,6 @@ function createMessagingStore() {
|
|
|
1125
1176
|
},
|
|
1126
1177
|
setActiveConversation(id) {
|
|
1127
1178
|
activeConversationId = id;
|
|
1128
|
-
conversations = normalizeConversations(conversations);
|
|
1129
1179
|
emit();
|
|
1130
1180
|
},
|
|
1131
1181
|
setConversationReadFailure(id, failure) {
|
|
@@ -1145,6 +1195,13 @@ function createMessagingStore() {
|
|
|
1145
1195
|
});
|
|
1146
1196
|
emit();
|
|
1147
1197
|
},
|
|
1198
|
+
clearThread(id) {
|
|
1199
|
+
if (!threads.has(id)) return;
|
|
1200
|
+
const next = new Map(threads);
|
|
1201
|
+
next.delete(id);
|
|
1202
|
+
threads = next;
|
|
1203
|
+
emit();
|
|
1204
|
+
},
|
|
1148
1205
|
prependOlder(id, messages, hasMoreOlder) {
|
|
1149
1206
|
const thread = threadFor(id);
|
|
1150
1207
|
const known = new Set(thread.messages.map((message) => message.id));
|
|
@@ -1197,40 +1254,48 @@ function createMessagingStore() {
|
|
|
1197
1254
|
emit();
|
|
1198
1255
|
},
|
|
1199
1256
|
applyLastMessage(message, args = {}) {
|
|
1200
|
-
const existing =
|
|
1201
|
-
(item) => item.conversation.id === message.conversationId
|
|
1202
|
-
);
|
|
1257
|
+
const existing = conversationSummaries.get(message.conversationId);
|
|
1203
1258
|
if (existing === void 0) return;
|
|
1204
1259
|
if (message.deliveryState === "sending" || message.deletedAt !== null) return;
|
|
1205
1260
|
if (existing.lastMessageAt !== null && message.createdAt < existing.lastMessageAt) return;
|
|
1206
|
-
const
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
unreadCount: shouldCount ? item.unreadCount + 1 : item.unreadCount
|
|
1217
|
-
} : item
|
|
1218
|
-
)
|
|
1219
|
-
);
|
|
1261
|
+
const shouldCount = args.incrementUnread === true;
|
|
1262
|
+
replaceOneSummary(message.conversationId, (item) => ({
|
|
1263
|
+
...item,
|
|
1264
|
+
lastMessageContent: message.content,
|
|
1265
|
+
lastMessageSenderId: message.senderId,
|
|
1266
|
+
lastMessageAt: message.createdAt,
|
|
1267
|
+
sortAt: message.createdAt,
|
|
1268
|
+
unreadCount: shouldCount ? item.unreadCount + 1 : item.unreadCount
|
|
1269
|
+
}));
|
|
1270
|
+
conversations = normalizeConversations(conversations);
|
|
1220
1271
|
emit();
|
|
1221
1272
|
},
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1273
|
+
captureConversationRead(id, at) {
|
|
1274
|
+
const item = conversationSummaries.get(id);
|
|
1275
|
+
return {
|
|
1276
|
+
at,
|
|
1277
|
+
unreadCount: item?.unreadCount ?? 0,
|
|
1278
|
+
revision: unreadRevisions.get(id) ?? 0
|
|
1279
|
+
};
|
|
1280
|
+
},
|
|
1281
|
+
markConversationRead(id, cutoff, canonicalUnreadCount = 0) {
|
|
1282
|
+
if (cutoff.revision <= (confirmedReadRevisions.get(id) ?? -1)) return;
|
|
1283
|
+
confirmedReadRevisions.set(id, cutoff.revision);
|
|
1284
|
+
replaceOneSummary(id, (item) => ({
|
|
1285
|
+
...item,
|
|
1286
|
+
// The repository re-counts after persisting the exact server cutoff.
|
|
1287
|
+
// If realtime advanced local state while that response was in flight,
|
|
1288
|
+
// retain at least the arrivals beyond the captured baseline.
|
|
1289
|
+
unreadCount: Math.max(
|
|
1290
|
+
0,
|
|
1291
|
+
canonicalUnreadCount,
|
|
1292
|
+
item.unreadCount - cutoff.unreadCount
|
|
1293
|
+
)
|
|
1294
|
+
}));
|
|
1226
1295
|
emit();
|
|
1227
1296
|
},
|
|
1228
1297
|
setUnreadCount(id, count) {
|
|
1229
|
-
|
|
1230
|
-
conversations.map(
|
|
1231
|
-
(item) => item.conversation.id === id ? { ...item, unreadCount: Math.max(0, count) } : item
|
|
1232
|
-
)
|
|
1233
|
-
);
|
|
1298
|
+
replaceOneSummary(id, (item) => ({ ...item, unreadCount: Math.max(0, count) }));
|
|
1234
1299
|
emit();
|
|
1235
1300
|
}
|
|
1236
1301
|
};
|
|
@@ -1267,21 +1332,57 @@ function createMessagingEngine(options) {
|
|
|
1267
1332
|
const conversationPageSize = options.conversationPageSize ?? 30;
|
|
1268
1333
|
const messagePageSize = options.messagePageSize ?? 50;
|
|
1269
1334
|
const openChannels = /* @__PURE__ */ new Map();
|
|
1335
|
+
const channelReadiness = /* @__PURE__ */ new Map();
|
|
1336
|
+
const settleChannelReadiness = /* @__PURE__ */ new Map();
|
|
1337
|
+
const readReceiptTails = /* @__PURE__ */ new Map();
|
|
1338
|
+
const persistedReadAt = /* @__PURE__ */ new Map();
|
|
1339
|
+
const initialReadDepth = /* @__PURE__ */ new Map();
|
|
1340
|
+
const initialReadGeneration = /* @__PURE__ */ new Map();
|
|
1341
|
+
const initialReadChanges = /* @__PURE__ */ new Map();
|
|
1270
1342
|
let inboxChannel = null;
|
|
1271
1343
|
let conversationCursor = null;
|
|
1272
1344
|
let disposed = false;
|
|
1273
1345
|
function report(event) {
|
|
1274
1346
|
options.onDiagnostic?.(event);
|
|
1275
1347
|
}
|
|
1276
|
-
function reportError(error, operation) {
|
|
1348
|
+
function reportError(error, operation, context = {}) {
|
|
1277
1349
|
const normalized = normalizeMessagingError(error, operation);
|
|
1278
1350
|
report({
|
|
1279
1351
|
// A missing session is a NORMAL lifecycle moment, not a red error. This
|
|
1280
1352
|
// one line is the cure for "909 captured errors in 0.6s".
|
|
1281
1353
|
level: normalized.code === "session-unavailable" ? "warn" : "error",
|
|
1282
1354
|
message: normalized.message,
|
|
1283
|
-
remedy: normalized.remedy
|
|
1355
|
+
remedy: normalized.remedy,
|
|
1356
|
+
code: normalized.code,
|
|
1357
|
+
operation,
|
|
1358
|
+
isRetryable: normalized.isRetryable,
|
|
1359
|
+
error: normalized,
|
|
1360
|
+
...context
|
|
1284
1361
|
});
|
|
1362
|
+
return normalized;
|
|
1363
|
+
}
|
|
1364
|
+
function persistRead(id, isLive = () => true) {
|
|
1365
|
+
const run = async () => {
|
|
1366
|
+
if (!isLive()) return;
|
|
1367
|
+
const at = store.snapshot().threads.get(id)?.latestAt ?? null;
|
|
1368
|
+
if (at === null || (persistedReadAt.get(id) ?? "") >= at) return;
|
|
1369
|
+
const cutoff = store.captureConversationRead(id, at);
|
|
1370
|
+
try {
|
|
1371
|
+
const canonicalUnreadCount = await repository.markRead(id, at);
|
|
1372
|
+
if (!isLive()) return;
|
|
1373
|
+
persistedReadAt.set(id, at);
|
|
1374
|
+
store.markConversationRead(id, cutoff, canonicalUnreadCount);
|
|
1375
|
+
} catch (error) {
|
|
1376
|
+
throw reportError(error, "markRead", { conversationId: id });
|
|
1377
|
+
}
|
|
1378
|
+
};
|
|
1379
|
+
const previous = readReceiptTails.get(id);
|
|
1380
|
+
const current = previous === void 0 ? run() : previous.catch(() => void 0).then(run);
|
|
1381
|
+
readReceiptTails.set(id, current);
|
|
1382
|
+
void current.finally(() => {
|
|
1383
|
+
if (readReceiptTails.get(id) === current) readReceiptTails.delete(id);
|
|
1384
|
+
}).catch(() => void 0);
|
|
1385
|
+
return current;
|
|
1285
1386
|
}
|
|
1286
1387
|
const outbox = createOutbox({
|
|
1287
1388
|
...options.outboxStorage !== void 0 ? { storage: options.outboxStorage } : {},
|
|
@@ -1366,6 +1467,137 @@ function createMessagingEngine(options) {
|
|
|
1366
1467
|
reportError(error, "backfillConversation");
|
|
1367
1468
|
}
|
|
1368
1469
|
}
|
|
1470
|
+
function applyLiveMessageChange(id, change) {
|
|
1471
|
+
if ((initialReadDepth.get(id) ?? 0) > 0) {
|
|
1472
|
+
const changes = initialReadChanges.get(id) ?? [];
|
|
1473
|
+
changes.push(change);
|
|
1474
|
+
initialReadChanges.set(id, changes);
|
|
1475
|
+
}
|
|
1476
|
+
if (change.kind === "delete") {
|
|
1477
|
+
store.removeMessage(id, change.messageId);
|
|
1478
|
+
} else {
|
|
1479
|
+
store.ingest(change.message);
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
function beginInitialRead(id) {
|
|
1483
|
+
const generation = (initialReadGeneration.get(id) ?? 0) + 1;
|
|
1484
|
+
initialReadGeneration.set(id, generation);
|
|
1485
|
+
initialReadDepth.set(id, (initialReadDepth.get(id) ?? 0) + 1);
|
|
1486
|
+
initialReadChanges.set(id, initialReadChanges.get(id) ?? []);
|
|
1487
|
+
return generation;
|
|
1488
|
+
}
|
|
1489
|
+
function finishInitialRead(id) {
|
|
1490
|
+
const depth = (initialReadDepth.get(id) ?? 1) - 1;
|
|
1491
|
+
if (depth > 0) {
|
|
1492
|
+
initialReadDepth.set(id, depth);
|
|
1493
|
+
return;
|
|
1494
|
+
}
|
|
1495
|
+
initialReadDepth.delete(id);
|
|
1496
|
+
initialReadChanges.delete(id);
|
|
1497
|
+
}
|
|
1498
|
+
function openIsLive(id, generation, channel) {
|
|
1499
|
+
return !disposed && initialReadGeneration.get(id) === generation && channel !== null && openChannels.get(id) === channel;
|
|
1500
|
+
}
|
|
1501
|
+
function installInitialPage(id, generation, page) {
|
|
1502
|
+
if (initialReadGeneration.get(id) !== generation) return false;
|
|
1503
|
+
const optimistic = (store.snapshot().threads.get(id)?.messages ?? []).filter(
|
|
1504
|
+
(item) => item.deliveryState === "sending" || item.deliveryState === "failed"
|
|
1505
|
+
);
|
|
1506
|
+
const confirmedClientKeys = new Set(
|
|
1507
|
+
page.items.flatMap(
|
|
1508
|
+
(item) => item.clientMessageId === null ? [] : [item.clientMessageId]
|
|
1509
|
+
)
|
|
1510
|
+
);
|
|
1511
|
+
store.setThread(id, page.items, { hasMoreOlder: page.hasMore });
|
|
1512
|
+
for (const change of initialReadChanges.get(id) ?? []) {
|
|
1513
|
+
if (change.kind === "delete") {
|
|
1514
|
+
store.removeMessage(id, change.messageId);
|
|
1515
|
+
} else {
|
|
1516
|
+
store.ingest(change.message);
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
store.ingestMany(
|
|
1520
|
+
optimistic.filter(
|
|
1521
|
+
(item) => item.clientMessageId === null || !confirmedClientKeys.has(item.clientMessageId)
|
|
1522
|
+
)
|
|
1523
|
+
);
|
|
1524
|
+
return true;
|
|
1525
|
+
}
|
|
1526
|
+
function ensureConversationChannel(id) {
|
|
1527
|
+
const existing = openChannels.get(id);
|
|
1528
|
+
if (existing !== void 0) {
|
|
1529
|
+
return channelReadiness.get(id) ?? Promise.resolve(existing.status() === "connected");
|
|
1530
|
+
}
|
|
1531
|
+
let markReady;
|
|
1532
|
+
const ready = new Promise((resolve) => {
|
|
1533
|
+
markReady = resolve;
|
|
1534
|
+
});
|
|
1535
|
+
const handle = manager.open({
|
|
1536
|
+
topic: conversationTopic(id),
|
|
1537
|
+
// Presence callbacks are JOIN-TIME in supabase-js. Declare the read-only
|
|
1538
|
+
// room shape on the first holder so later holders do not create a gap.
|
|
1539
|
+
presence: { publish: false, state: {} },
|
|
1540
|
+
postgresChanges: [
|
|
1541
|
+
{
|
|
1542
|
+
event: "*",
|
|
1543
|
+
schema: "communication",
|
|
1544
|
+
table: "dm_messages",
|
|
1545
|
+
filter: `conversation_id=eq.${id}`,
|
|
1546
|
+
rowId: (row) => typeof row["id"] === "string" ? row["id"] : void 0,
|
|
1547
|
+
fingerprint: (row) => typeof row["content"] === "string" ? row["content"] : void 0,
|
|
1548
|
+
updatedAtField: "updated_at",
|
|
1549
|
+
updatedByField: "updated_by",
|
|
1550
|
+
onChange: ({ payload, row }) => {
|
|
1551
|
+
if (row === null) return;
|
|
1552
|
+
if (payload?.eventType === "DELETE" || typeof row["deleted_at"] === "string" && row["deleted_at"].length > 0) {
|
|
1553
|
+
const rawId = row["id"];
|
|
1554
|
+
if (typeof rawId !== "string" || rawId.length === 0) {
|
|
1555
|
+
reportError(
|
|
1556
|
+
new Error("A message deletion payload carried no message id."),
|
|
1557
|
+
"conversationMessageDelete",
|
|
1558
|
+
{ conversationId: id }
|
|
1559
|
+
);
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
applyLiveMessageChange(id, {
|
|
1563
|
+
kind: "delete",
|
|
1564
|
+
messageId: asMessageId(rawId)
|
|
1565
|
+
});
|
|
1566
|
+
return;
|
|
1567
|
+
}
|
|
1568
|
+
const message = projectMessage(row, identity.organizationId);
|
|
1569
|
+
applyLiveMessageChange(id, { kind: "upsert", message });
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
],
|
|
1573
|
+
broadcast: [
|
|
1574
|
+
{
|
|
1575
|
+
event: MESSAGING_EVENTS.message,
|
|
1576
|
+
onMessage: ({ data }) => {
|
|
1577
|
+
try {
|
|
1578
|
+
const message = projectBroadcastMessage(data);
|
|
1579
|
+
applyLiveMessageChange(id, { kind: "upsert", message });
|
|
1580
|
+
} catch (error) {
|
|
1581
|
+
reportError(error, "broadcastMessage");
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
],
|
|
1586
|
+
onStatusChange: (status) => {
|
|
1587
|
+
if (status === "connected") markReady?.(true);
|
|
1588
|
+
if (status === "disconnected") markReady?.(false);
|
|
1589
|
+
},
|
|
1590
|
+
onBackfill: () => {
|
|
1591
|
+
void backfillConversation(id);
|
|
1592
|
+
outbox.flush();
|
|
1593
|
+
}
|
|
1594
|
+
});
|
|
1595
|
+
openChannels.set(id, handle);
|
|
1596
|
+
channelReadiness.set(id, ready);
|
|
1597
|
+
settleChannelReadiness.set(id, (connected) => markReady?.(connected));
|
|
1598
|
+
if (handle.status() === "connected") markReady?.(true);
|
|
1599
|
+
return ready;
|
|
1600
|
+
}
|
|
1369
1601
|
const engine = {
|
|
1370
1602
|
store,
|
|
1371
1603
|
outbox,
|
|
@@ -1391,7 +1623,9 @@ function createMessagingEngine(options) {
|
|
|
1391
1623
|
onChange: ({ row }) => {
|
|
1392
1624
|
if (row === null) return;
|
|
1393
1625
|
const message = projectMessage(row, identity.organizationId);
|
|
1394
|
-
const known = store.snapshot().
|
|
1626
|
+
const known = store.snapshot().conversationSummaries.has(
|
|
1627
|
+
message.conversationId
|
|
1628
|
+
);
|
|
1395
1629
|
if (!known) {
|
|
1396
1630
|
void reloadInbox().catch((error) => reportError(error, "inboxRefresh"));
|
|
1397
1631
|
return;
|
|
@@ -1399,6 +1633,9 @@ function createMessagingEngine(options) {
|
|
|
1399
1633
|
store.ingest(message);
|
|
1400
1634
|
const isMine = message.senderId === identity.userId;
|
|
1401
1635
|
store.applyLastMessage(message, { incrementUnread: !isMine });
|
|
1636
|
+
if (!isMine && store.snapshot().activeConversationId === message.conversationId) {
|
|
1637
|
+
void persistRead(message.conversationId).catch(() => void 0);
|
|
1638
|
+
}
|
|
1402
1639
|
if (!isMine) options.onIncoming?.(message);
|
|
1403
1640
|
}
|
|
1404
1641
|
},
|
|
@@ -1463,18 +1700,35 @@ function createMessagingEngine(options) {
|
|
|
1463
1700
|
},
|
|
1464
1701
|
async openConversation(id) {
|
|
1465
1702
|
store.setActiveConversation(id);
|
|
1703
|
+
const generation = beginInitialRead(id);
|
|
1704
|
+
let ownedChannel = null;
|
|
1466
1705
|
try {
|
|
1706
|
+
const connected = await ensureConversationChannel(id);
|
|
1707
|
+
ownedChannel = openChannels.get(id) ?? null;
|
|
1708
|
+
if (!connected || !openIsLive(id, generation, ownedChannel)) return;
|
|
1467
1709
|
const page = await repository.listMessages(id, { limit: messagePageSize });
|
|
1468
|
-
if (
|
|
1469
|
-
|
|
1470
|
-
|
|
1710
|
+
if (!openIsLive(id, generation, ownedChannel)) return;
|
|
1711
|
+
if (!installInitialPage(id, generation, page)) return;
|
|
1712
|
+
const missingSummary = !store.snapshot().conversationSummaries.has(id);
|
|
1713
|
+
const directSummary = missingSummary ? await repository.getConversationSummary(id) : null;
|
|
1714
|
+
if (!openIsLive(id, generation, ownedChannel)) return;
|
|
1471
1715
|
store.setConversationReadFailure(id, null);
|
|
1472
|
-
|
|
1716
|
+
if (directSummary !== null) store.setConversationSummary(directSummary);
|
|
1717
|
+
await persistRead(
|
|
1718
|
+
id,
|
|
1719
|
+
() => openIsLive(id, generation, ownedChannel)
|
|
1720
|
+
).catch(() => void 0);
|
|
1473
1721
|
} catch (error) {
|
|
1722
|
+
if (!openIsLive(id, generation, ownedChannel)) return;
|
|
1474
1723
|
const normalized = normalizeMessagingError(error, "openConversation");
|
|
1475
1724
|
const unavailable = normalized.code === "not-found" || normalized.code === "forbidden";
|
|
1476
1725
|
store.setConversationReadFailure(id, { error: normalized, unavailable });
|
|
1477
1726
|
if (unavailable) {
|
|
1727
|
+
openChannels.get(id)?.close();
|
|
1728
|
+
openChannels.delete(id);
|
|
1729
|
+
settleChannelReadiness.delete(id);
|
|
1730
|
+
channelReadiness.delete(id);
|
|
1731
|
+
store.clearThread(id);
|
|
1478
1732
|
report({
|
|
1479
1733
|
level: "warn",
|
|
1480
1734
|
message: normalized.message,
|
|
@@ -1483,53 +1737,17 @@ function createMessagingEngine(options) {
|
|
|
1483
1737
|
return;
|
|
1484
1738
|
}
|
|
1485
1739
|
reportError(normalized, "openConversation");
|
|
1740
|
+
} finally {
|
|
1741
|
+
finishInitialRead(id);
|
|
1486
1742
|
}
|
|
1487
|
-
if (openChannels.has(id) || disposed) return;
|
|
1488
|
-
const handle = manager.open({
|
|
1489
|
-
topic: conversationTopic(id),
|
|
1490
|
-
// Presence callbacks are JOIN-TIME in supabase-js. Declare the
|
|
1491
|
-
// read-only room shape on the first holder (the engine) so the
|
|
1492
|
-
// composer/typist/online hooks can attach without forcing a rebuild
|
|
1493
|
-
// after this durable channel has already subscribed.
|
|
1494
|
-
presence: { publish: false, state: {} },
|
|
1495
|
-
postgresChanges: [
|
|
1496
|
-
{
|
|
1497
|
-
event: "*",
|
|
1498
|
-
schema: "communication",
|
|
1499
|
-
table: "dm_messages",
|
|
1500
|
-
filter: `conversation_id=eq.${id}`,
|
|
1501
|
-
rowId: (row) => typeof row["id"] === "string" ? row["id"] : void 0,
|
|
1502
|
-
fingerprint: (row) => typeof row["content"] === "string" ? row["content"] : void 0,
|
|
1503
|
-
updatedAtField: "updated_at",
|
|
1504
|
-
updatedByField: "updated_by",
|
|
1505
|
-
onChange: ({ row }) => {
|
|
1506
|
-
if (row === null) return;
|
|
1507
|
-
store.ingest(projectMessage(row, identity.organizationId));
|
|
1508
|
-
}
|
|
1509
|
-
}
|
|
1510
|
-
],
|
|
1511
|
-
broadcast: [
|
|
1512
|
-
{
|
|
1513
|
-
event: MESSAGING_EVENTS.message,
|
|
1514
|
-
onMessage: ({ data }) => {
|
|
1515
|
-
try {
|
|
1516
|
-
store.ingest(projectBroadcastMessage(data));
|
|
1517
|
-
} catch (error) {
|
|
1518
|
-
reportError(error, "broadcastMessage");
|
|
1519
|
-
}
|
|
1520
|
-
}
|
|
1521
|
-
}
|
|
1522
|
-
],
|
|
1523
|
-
onBackfill: () => {
|
|
1524
|
-
void backfillConversation(id);
|
|
1525
|
-
outbox.flush();
|
|
1526
|
-
}
|
|
1527
|
-
});
|
|
1528
|
-
openChannels.set(id, handle);
|
|
1529
1743
|
},
|
|
1530
1744
|
closeConversation(id) {
|
|
1745
|
+
initialReadGeneration.set(id, (initialReadGeneration.get(id) ?? 0) + 1);
|
|
1531
1746
|
openChannels.get(id)?.close();
|
|
1532
1747
|
openChannels.delete(id);
|
|
1748
|
+
settleChannelReadiness.get(id)?.(false);
|
|
1749
|
+
settleChannelReadiness.delete(id);
|
|
1750
|
+
channelReadiness.delete(id);
|
|
1533
1751
|
if (store.snapshot().activeConversationId === id) {
|
|
1534
1752
|
store.setActiveConversation(null);
|
|
1535
1753
|
}
|
|
@@ -1581,12 +1799,7 @@ function createMessagingEngine(options) {
|
|
|
1581
1799
|
}
|
|
1582
1800
|
},
|
|
1583
1801
|
async markRead(id) {
|
|
1584
|
-
|
|
1585
|
-
try {
|
|
1586
|
-
await repository.markRead(id);
|
|
1587
|
-
} catch (error) {
|
|
1588
|
-
reportError(error, "markRead");
|
|
1589
|
-
}
|
|
1802
|
+
await persistRead(id);
|
|
1590
1803
|
},
|
|
1591
1804
|
async startDirectConversation(otherUserId) {
|
|
1592
1805
|
const id = await repository.getOrCreateDirectConversation(otherUserId);
|
|
@@ -1595,9 +1808,15 @@ function createMessagingEngine(options) {
|
|
|
1595
1808
|
},
|
|
1596
1809
|
dispose() {
|
|
1597
1810
|
disposed = true;
|
|
1811
|
+
openChannels.forEach((_handle, id) => {
|
|
1812
|
+
initialReadGeneration.set(id, (initialReadGeneration.get(id) ?? 0) + 1);
|
|
1813
|
+
});
|
|
1598
1814
|
outbox.dispose();
|
|
1599
1815
|
openChannels.forEach((handle) => handle.close());
|
|
1600
1816
|
openChannels.clear();
|
|
1817
|
+
settleChannelReadiness.forEach((settle) => settle(false));
|
|
1818
|
+
settleChannelReadiness.clear();
|
|
1819
|
+
channelReadiness.clear();
|
|
1601
1820
|
inboxChannel?.close();
|
|
1602
1821
|
inboxChannel = null;
|
|
1603
1822
|
}
|
|
@@ -1854,6 +2073,7 @@ var RPCS = {
|
|
|
1854
2073
|
isParticipant: "is_dm_participant"
|
|
1855
2074
|
};
|
|
1856
2075
|
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";
|
|
2076
|
+
var PARTICIPANT_COLUMNS = "conversation_id,user_id,role,joined_at,last_read_at,is_muted,is_archived";
|
|
1857
2077
|
var CONVERSATION_COLUMNS = "id,type,group_name,group_image_url,created_by,organization_id,created_at,updated_at,metadata";
|
|
1858
2078
|
function optionalOrg(organizationId) {
|
|
1859
2079
|
return typeof organizationId === "string" && organizationId.length > 0 ? organizationId : null;
|
|
@@ -1923,6 +2143,14 @@ function createMessagingRepository(options) {
|
|
|
1923
2143
|
function actingUser(operation) {
|
|
1924
2144
|
return withSessionRetry(operation, () => actingUserId());
|
|
1925
2145
|
}
|
|
2146
|
+
async function updateLiveParticipant(operation, conversationId, userId, patch) {
|
|
2147
|
+
const destination = await organizationOfExisting(conversationId);
|
|
2148
|
+
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();
|
|
2149
|
+
if (error !== null) throw normalizeMessagingError(error, operation);
|
|
2150
|
+
if (data === null) {
|
|
2151
|
+
throw writeNotApplied(operation, "participant update returned no live row");
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
1926
2154
|
async function rpc(fn, args, operation) {
|
|
1927
2155
|
const { data, error } = await rpcDb().rpc(fn, args);
|
|
1928
2156
|
if (error !== null) throw normalizeMessagingError(error, operation);
|
|
@@ -1945,6 +2173,33 @@ function createMessagingRepository(options) {
|
|
|
1945
2173
|
});
|
|
1946
2174
|
return found;
|
|
1947
2175
|
}
|
|
2176
|
+
async function allLiveParticipants(conversationId) {
|
|
2177
|
+
const operation = "getConversationSummary";
|
|
2178
|
+
const pageSize = 1e3;
|
|
2179
|
+
const rows = [];
|
|
2180
|
+
for (let from = 0; ; from += pageSize) {
|
|
2181
|
+
const response = await withSessionRetry(
|
|
2182
|
+
operation,
|
|
2183
|
+
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)
|
|
2184
|
+
);
|
|
2185
|
+
if (response.error !== null) {
|
|
2186
|
+
throw normalizeMessagingError(response.error, operation);
|
|
2187
|
+
}
|
|
2188
|
+
const total = response.count;
|
|
2189
|
+
if (typeof total !== "number" || !Number.isInteger(total) || total < 0) {
|
|
2190
|
+
throw invalidResponse(operation, "participant read returned no exact count");
|
|
2191
|
+
}
|
|
2192
|
+
const page = response.data ?? [];
|
|
2193
|
+
rows.push(...page);
|
|
2194
|
+
if (rows.length >= total) return rows;
|
|
2195
|
+
if (page.length === 0) {
|
|
2196
|
+
throw invalidResponse(
|
|
2197
|
+
operation,
|
|
2198
|
+
`participant read stopped at ${rows.length} of ${total}`
|
|
2199
|
+
);
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
1948
2203
|
const conversationOrganizations = /* @__PURE__ */ new Map();
|
|
1949
2204
|
async function organizationOfExisting(conversationId) {
|
|
1950
2205
|
const known = conversationOrganizations.get(conversationId);
|
|
@@ -2056,6 +2311,78 @@ function createMessagingRepository(options) {
|
|
|
2056
2311
|
);
|
|
2057
2312
|
return summary.conversation;
|
|
2058
2313
|
},
|
|
2314
|
+
async getConversationSummary(id) {
|
|
2315
|
+
const operation = "getConversationSummary";
|
|
2316
|
+
const me = await actingUser(operation);
|
|
2317
|
+
const [conversationResult, participantRows, latestResult, unreadRaw] = await withSessionRetry(
|
|
2318
|
+
operation,
|
|
2319
|
+
() => Promise.all([
|
|
2320
|
+
db().from(TABLES.conversations).select(CONVERSATION_COLUMNS).eq("id", id).single(),
|
|
2321
|
+
allLiveParticipants(id),
|
|
2322
|
+
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(),
|
|
2323
|
+
rpc(
|
|
2324
|
+
RPCS.unreadCount,
|
|
2325
|
+
{ p_conversation_id: id, p_user_id: me },
|
|
2326
|
+
operation
|
|
2327
|
+
)
|
|
2328
|
+
])
|
|
2329
|
+
);
|
|
2330
|
+
if (conversationResult.error !== null) {
|
|
2331
|
+
throw normalizeMessagingError(conversationResult.error, operation);
|
|
2332
|
+
}
|
|
2333
|
+
if (latestResult.error !== null) {
|
|
2334
|
+
throw normalizeMessagingError(latestResult.error, operation);
|
|
2335
|
+
}
|
|
2336
|
+
const conversation = conversationResult.data;
|
|
2337
|
+
if (conversation === null) {
|
|
2338
|
+
throw invalidResponse(operation, `conversation ${id} returned no row`);
|
|
2339
|
+
}
|
|
2340
|
+
if (typeof unreadRaw !== "number" || !Number.isFinite(unreadRaw) || unreadRaw < 0) {
|
|
2341
|
+
throw invalidResponse(operation, `${RPCS.unreadCount} returned ${JSON.stringify(unreadRaw)}`);
|
|
2342
|
+
}
|
|
2343
|
+
const participantIds = participantRows.flatMap((row) => {
|
|
2344
|
+
const value = row["user_id"];
|
|
2345
|
+
return typeof value === "string" && value.length > 0 ? [projectUserSummary({ user_id: value }).userId] : [];
|
|
2346
|
+
});
|
|
2347
|
+
const profiles = await getUsers(participantIds);
|
|
2348
|
+
const participants = participantIds.map((userId) => {
|
|
2349
|
+
const profile = profiles.get(userId);
|
|
2350
|
+
return profile === void 0 ? {
|
|
2351
|
+
user_id: userId,
|
|
2352
|
+
display_name: userId
|
|
2353
|
+
} : {
|
|
2354
|
+
user_id: profile.userId,
|
|
2355
|
+
display_name: profile.displayName,
|
|
2356
|
+
email: profile.email,
|
|
2357
|
+
avatar_url: profile.avatarUrl,
|
|
2358
|
+
is_agent: profile.isAgent
|
|
2359
|
+
};
|
|
2360
|
+
});
|
|
2361
|
+
const viewer = participantRows.find((row) => row["user_id"] === me);
|
|
2362
|
+
const latest = latestResult.data;
|
|
2363
|
+
return projectConversationSummary(
|
|
2364
|
+
{
|
|
2365
|
+
conversation_id: conversation["id"],
|
|
2366
|
+
conversation_type: conversation["type"],
|
|
2367
|
+
group_name: conversation["group_name"],
|
|
2368
|
+
group_image_url: conversation["group_image_url"],
|
|
2369
|
+
conversation_created_at: conversation["created_at"],
|
|
2370
|
+
conversation_updated_at: conversation["updated_at"],
|
|
2371
|
+
organization_id: conversation["organization_id"],
|
|
2372
|
+
created_by: conversation["created_by"],
|
|
2373
|
+
metadata: conversation["metadata"],
|
|
2374
|
+
participants,
|
|
2375
|
+
last_message_content: latest?.["content"] ?? null,
|
|
2376
|
+
last_message_sender_id: latest?.["sender_id"] ?? null,
|
|
2377
|
+
last_message_at: latest?.["created_at"] ?? null,
|
|
2378
|
+
unread_count: Math.floor(unreadRaw),
|
|
2379
|
+
is_muted: viewer?.["is_muted"] ?? false,
|
|
2380
|
+
is_archived: viewer?.["is_archived"] ?? false
|
|
2381
|
+
},
|
|
2382
|
+
me,
|
|
2383
|
+
org
|
|
2384
|
+
);
|
|
2385
|
+
},
|
|
2059
2386
|
async listMessages(conversationId, args = {}) {
|
|
2060
2387
|
const limit = args.limit ?? 50;
|
|
2061
2388
|
const operation = "listMessages";
|
|
@@ -2201,8 +2528,22 @@ function createMessagingRepository(options) {
|
|
|
2201
2528
|
async markRead(conversationId, at) {
|
|
2202
2529
|
const operation = "markRead";
|
|
2203
2530
|
const me = await actingUser(operation);
|
|
2204
|
-
|
|
2205
|
-
|
|
2531
|
+
await updateLiveParticipant(operation, conversationId, me, {
|
|
2532
|
+
last_read_at: at,
|
|
2533
|
+
updated_by: me
|
|
2534
|
+
});
|
|
2535
|
+
const unread = await withSessionRetry(
|
|
2536
|
+
operation,
|
|
2537
|
+
() => rpc(
|
|
2538
|
+
RPCS.unreadCount,
|
|
2539
|
+
{ p_conversation_id: conversationId, p_user_id: me },
|
|
2540
|
+
operation
|
|
2541
|
+
)
|
|
2542
|
+
);
|
|
2543
|
+
if (typeof unread !== "number" || !Number.isFinite(unread) || unread < 0) {
|
|
2544
|
+
throw invalidResponse(operation, `${RPCS.unreadCount} returned ${JSON.stringify(unread)}`);
|
|
2545
|
+
}
|
|
2546
|
+
return Math.floor(unread);
|
|
2206
2547
|
},
|
|
2207
2548
|
async setConversationFlags(conversationId, flags) {
|
|
2208
2549
|
const operation = "setConversationFlags";
|
|
@@ -2210,8 +2551,7 @@ function createMessagingRepository(options) {
|
|
|
2210
2551
|
const patch = { updated_by: me };
|
|
2211
2552
|
if (flags.isMuted !== void 0) patch["is_muted"] = flags.isMuted;
|
|
2212
2553
|
if (flags.isArchived !== void 0) patch["is_archived"] = flags.isArchived;
|
|
2213
|
-
|
|
2214
|
-
if (error !== null) throw normalizeMessagingError(error, operation);
|
|
2554
|
+
await updateLiveParticipant(operation, conversationId, me, patch);
|
|
2215
2555
|
},
|
|
2216
2556
|
async addMembers(conversationId, memberIds) {
|
|
2217
2557
|
const operation = "addMembers";
|
|
@@ -2231,14 +2571,18 @@ function createMessagingRepository(options) {
|
|
|
2231
2571
|
async removeMember(conversationId, memberId) {
|
|
2232
2572
|
const operation = "removeMember";
|
|
2233
2573
|
const me = await actingUser(operation);
|
|
2234
|
-
|
|
2235
|
-
|
|
2574
|
+
await updateLiveParticipant(operation, conversationId, memberId, {
|
|
2575
|
+
deleted_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2576
|
+
updated_by: me
|
|
2577
|
+
});
|
|
2236
2578
|
},
|
|
2237
2579
|
async setMemberRole(conversationId, memberId, role) {
|
|
2238
2580
|
const operation = "setMemberRole";
|
|
2239
2581
|
const me = await actingUser(operation);
|
|
2240
|
-
|
|
2241
|
-
|
|
2582
|
+
await updateLiveParticipant(operation, conversationId, memberId, {
|
|
2583
|
+
role,
|
|
2584
|
+
updated_by: me
|
|
2585
|
+
});
|
|
2242
2586
|
},
|
|
2243
2587
|
getUser(userId) {
|
|
2244
2588
|
const operation = "getUser";
|
|
@@ -2327,6 +2671,7 @@ export {
|
|
|
2327
2671
|
splitText,
|
|
2328
2672
|
summarizeText,
|
|
2329
2673
|
toMessagingArchiveFilter,
|
|
2330
|
-
unreadCutoff
|
|
2674
|
+
unreadCutoff,
|
|
2675
|
+
writeNotApplied
|
|
2331
2676
|
};
|
|
2332
2677
|
//# sourceMappingURL=index.js.map
|