@ai-matrx/messaging 0.15.82 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +46 -1
- package/dist/index.cjs +171 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +71 -2
- package/dist/index.d.ts +71 -2
- package/dist/index.js +171 -9
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +206 -10
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +73 -2
- package/dist/react.d.ts +73 -2
- package/dist/react.js +206 -10
- package/dist/react.js.map +1 -1
- package/dist/styles.css +15 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,12 +1,57 @@
|
|
|
1
1
|
# Changelog — `@ai-matrx/messaging`
|
|
2
2
|
|
|
3
|
+
## 0.16.0
|
|
4
|
+
|
|
5
|
+
**DM soft expiry.** A direct message can carry `softExpiresAt` (column
|
|
6
|
+
`communication.dm_messages.soft_expires_at`). Past that instant the message is
|
|
7
|
+
LAPSED: still readable and actionable, never hidden or edited, and it no longer
|
|
8
|
+
counts as unread. The reader's `last_read_at` watermark is never moved for it.
|
|
9
|
+
Lapsed is derived live, never stamped — no sweeper, no schedule.
|
|
10
|
+
|
|
11
|
+
- `Message.softExpiresAt: string | null` (required) and
|
|
12
|
+
`DraftMessage.softExpiresAt?: string`; every message read selects
|
|
13
|
+
`soft_expires_at`, and `insertMessage` writes it only when a draft sets it.
|
|
14
|
+
- `isLapsed(message, now)` — the ONE lapse rule — and `softExpiryTime(message)`,
|
|
15
|
+
both exported from the core entry.
|
|
16
|
+
- Store: an incoming message that has already lapsed never increments
|
|
17
|
+
`unreadCount` (store rule 6) — judged on the SERVER's clock (below).
|
|
18
|
+
- Engine: ONE timer for the soonest pending expiry across the loaded inbox. When
|
|
19
|
+
it fires, the due conversations' counts are RE-READ from the server
|
|
20
|
+
(`get_dm_unread_count` already excludes lapsed messages), so
|
|
21
|
+
`totalUnreadConversations` and every badge follow without a reload. Lapse is
|
|
22
|
+
judged on the server's clock: each `pendingSoftExpiries` answer carries the
|
|
23
|
+
database's `now()` and the engine keeps the offset, so a browser clock that runs
|
|
24
|
+
fast never hides a message the server still counts; an expiry already past
|
|
25
|
+
re-reads the server's count at once instead of subtracting locally.
|
|
26
|
+
- Repository: `unreadCount(conversationId)` (read only) and
|
|
27
|
+
`pendingSoftExpiries(conversationIds)` → `{ byConversation, serverNow }`, through
|
|
28
|
+
`public.get_dm_pending_soft_expiries` — ONE answer per conversation (its soonest
|
|
29
|
+
pending expiry), chunked 200 conversations per call, so a busy thread can never
|
|
30
|
+
starve its neighbours out of a row cap.
|
|
31
|
+
- `MessageBubble`: a lapsed message renders muted and outlined with a small
|
|
32
|
+
clock + "Past" marker in its meta row; its chips and links keep working.
|
|
33
|
+
`ConversationView` drives it from one clock per thread.
|
|
34
|
+
|
|
35
|
+
**Requires the database.** Every message read now names `soft_expires_at`, and
|
|
36
|
+
`pendingSoftExpiries` calls `public.get_dm_pending_soft_expiries`; against a
|
|
37
|
+
database without them DM reads fail. matrx-frontend
|
|
38
|
+
`migrations/dm_soft_expiry_and_dm_pairs_with_email.sql` is on production since 2026-10-04.
|
|
39
|
+
|
|
40
|
+
### Consumer action
|
|
41
|
+
|
|
42
|
+
- Any code that constructs a `Message` literal (fixtures, demos, optimistic
|
|
43
|
+
stand-ins) adds `softExpiresAt: null`.
|
|
44
|
+
- A sender that wants a soft expiry passes `softExpiresAt` (ISO instant) on the
|
|
45
|
+
draft — never compare the field directly; ask `isLapsed`.
|
|
46
|
+
- A custom repository implementing `MessagingRepository` adds `unreadCount` and
|
|
47
|
+
`pendingSoftExpiries`.
|
|
48
|
+
|
|
3
49
|
## 0.15.82
|
|
4
50
|
### The unread badge is a content-sized pill again
|
|
5
51
|
|
|
6
52
|
- `.mx-msg__row-preview span` also matched the unread badge (a span in the same row) and stretched a "1" to a ~100px bar; the selector is now `.mx-msg__row-preview > span:first-child`, the preview text only.
|
|
7
53
|
- Consumer action: delete any host override pinning `.mx-msg__row-preview > .mx-msg__badge` to `flex: 0 0 auto`.
|
|
8
54
|
|
|
9
|
-
|
|
10
55
|
## 0.15.81
|
|
11
56
|
|
|
12
57
|
Automatic changed-only republish (docs/metadata drift since the last tag — see
|
package/dist/index.cjs
CHANGED
|
@@ -56,6 +56,7 @@ __export(src_exports, {
|
|
|
56
56
|
groupMessages: () => groupMessages,
|
|
57
57
|
inboxTopic: () => inboxTopic,
|
|
58
58
|
invalidResponse: () => invalidResponse,
|
|
59
|
+
isLapsed: () => isLapsed,
|
|
59
60
|
isSameDay: () => isSameDay,
|
|
60
61
|
messagingClientId: () => messagingClientId,
|
|
61
62
|
normalizeMessagingError: () => normalizeMessagingError,
|
|
@@ -67,6 +68,7 @@ __export(src_exports, {
|
|
|
67
68
|
projectParticipantRole: () => projectParticipantRole,
|
|
68
69
|
projectUserSummary: () => projectUserSummary,
|
|
69
70
|
resolveActor: () => resolveActor,
|
|
71
|
+
softExpiryTime: () => softExpiryTime,
|
|
70
72
|
splitText: () => splitText,
|
|
71
73
|
summarizeText: () => summarizeText,
|
|
72
74
|
toMessagingArchiveFilter: () => toMessagingArchiveFilter,
|
|
@@ -973,7 +975,8 @@ function projectBroadcastMessage(value) {
|
|
|
973
975
|
action: projectMessageAction(record["action"]),
|
|
974
976
|
attachments: projectAttachmentList(record["attachments"]),
|
|
975
977
|
references: projectReferenceList(record["references"]),
|
|
976
|
-
metadata
|
|
978
|
+
metadata,
|
|
979
|
+
softExpiresAt: str(record, "softExpiresAt")
|
|
977
980
|
};
|
|
978
981
|
}
|
|
979
982
|
function projectMessage(row, fallbackOrganizationId) {
|
|
@@ -1003,7 +1006,8 @@ function projectMessage(row, fallbackOrganizationId) {
|
|
|
1003
1006
|
action,
|
|
1004
1007
|
attachments: projectAttachments(metadata),
|
|
1005
1008
|
references: projectReferences(metadata),
|
|
1006
|
-
metadata
|
|
1009
|
+
metadata,
|
|
1010
|
+
softExpiresAt: str(row, "soft_expires_at")
|
|
1007
1011
|
};
|
|
1008
1012
|
}
|
|
1009
1013
|
function projectParticipantRole(value) {
|
|
@@ -1056,6 +1060,18 @@ function projectConversationSummary(row, viewerId, fallbackOrganizationId) {
|
|
|
1056
1060
|
};
|
|
1057
1061
|
}
|
|
1058
1062
|
|
|
1063
|
+
// src/core/lapse.ts
|
|
1064
|
+
function softExpiryTime(message) {
|
|
1065
|
+
if (message.softExpiresAt === null) return null;
|
|
1066
|
+
const parsed = Date.parse(message.softExpiresAt);
|
|
1067
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
1068
|
+
}
|
|
1069
|
+
function isLapsed(message, now = Date.now()) {
|
|
1070
|
+
const at = softExpiryTime(message);
|
|
1071
|
+
if (at === null) return false;
|
|
1072
|
+
return at <= (typeof now === "number" ? now : now.getTime());
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1059
1075
|
// src/core/types.ts
|
|
1060
1076
|
var asConversationId = (value) => value;
|
|
1061
1077
|
var asMessageId = (value) => value;
|
|
@@ -1093,7 +1109,8 @@ function insertOrdered(messages, message) {
|
|
|
1093
1109
|
next.splice(index, 0, message);
|
|
1094
1110
|
return next;
|
|
1095
1111
|
}
|
|
1096
|
-
function createMessagingStore() {
|
|
1112
|
+
function createMessagingStore(options = {}) {
|
|
1113
|
+
const now = options.now ?? Date.now;
|
|
1097
1114
|
let conversations = [];
|
|
1098
1115
|
let conversationSummaries = /* @__PURE__ */ new Map();
|
|
1099
1116
|
let hasMoreConversations = false;
|
|
@@ -1330,7 +1347,7 @@ function createMessagingStore() {
|
|
|
1330
1347
|
if (existing === void 0) return;
|
|
1331
1348
|
if (message.deliveryState === "sending" || message.deletedAt !== null) return;
|
|
1332
1349
|
if (existing.lastMessageAt !== null && message.createdAt < existing.lastMessageAt) return;
|
|
1333
|
-
const shouldCount = args.incrementUnread === true;
|
|
1350
|
+
const shouldCount = args.incrementUnread === true && !isLapsed(message, now());
|
|
1334
1351
|
replaceOneSummary(message.conversationId, (item) => ({
|
|
1335
1352
|
...item,
|
|
1336
1353
|
lastMessageContent: message.content,
|
|
@@ -1393,14 +1410,25 @@ function optimisticMessage(args) {
|
|
|
1393
1410
|
action: args.action ?? null,
|
|
1394
1411
|
attachments: args.attachments ?? [],
|
|
1395
1412
|
references: args.references ?? [],
|
|
1396
|
-
metadata: {}
|
|
1413
|
+
metadata: {},
|
|
1414
|
+
softExpiresAt: args.softExpiresAt ?? null
|
|
1397
1415
|
};
|
|
1398
1416
|
}
|
|
1399
1417
|
|
|
1400
1418
|
// src/core/engine.ts
|
|
1419
|
+
var SOFT_EXPIRY_GRACE_MS = 2e3;
|
|
1420
|
+
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
1401
1421
|
function createMessagingEngine(options) {
|
|
1402
1422
|
const { repository, manager, identity } = options;
|
|
1403
|
-
const
|
|
1423
|
+
const browserNow = options.now ?? Date.now;
|
|
1424
|
+
let serverClockOffsetMs = 0;
|
|
1425
|
+
const now = () => browserNow() + serverClockOffsetMs;
|
|
1426
|
+
function learnServerClock(serverNow) {
|
|
1427
|
+
if (serverNow === null) return;
|
|
1428
|
+
const server = Date.parse(serverNow);
|
|
1429
|
+
if (Number.isFinite(server)) serverClockOffsetMs = server - browserNow();
|
|
1430
|
+
}
|
|
1431
|
+
const store = createMessagingStore({ now });
|
|
1404
1432
|
const conversationPageSize = options.conversationPageSize ?? 30;
|
|
1405
1433
|
const messagePageSize = options.messagePageSize ?? 50;
|
|
1406
1434
|
const openChannels = /* @__PURE__ */ new Map();
|
|
@@ -1414,6 +1442,8 @@ function createMessagingEngine(options) {
|
|
|
1414
1442
|
let inboxChannel = null;
|
|
1415
1443
|
let conversationCursor = null;
|
|
1416
1444
|
let disposed = false;
|
|
1445
|
+
const pendingExpiry = /* @__PURE__ */ new Map();
|
|
1446
|
+
let expiryTimer = null;
|
|
1417
1447
|
function report(event) {
|
|
1418
1448
|
options.onDiagnostic?.(event);
|
|
1419
1449
|
}
|
|
@@ -1456,6 +1486,72 @@ function createMessagingEngine(options) {
|
|
|
1456
1486
|
}).catch(() => void 0);
|
|
1457
1487
|
return current;
|
|
1458
1488
|
}
|
|
1489
|
+
function scheduleExpiryTimer() {
|
|
1490
|
+
if (expiryTimer !== null) {
|
|
1491
|
+
clearTimeout(expiryTimer);
|
|
1492
|
+
expiryTimer = null;
|
|
1493
|
+
}
|
|
1494
|
+
if (disposed || pendingExpiry.size === 0) return;
|
|
1495
|
+
const soonest = Math.min(...pendingExpiry.values());
|
|
1496
|
+
const delay = Math.min(
|
|
1497
|
+
MAX_TIMER_DELAY_MS,
|
|
1498
|
+
Math.max(0, soonest + SOFT_EXPIRY_GRACE_MS - now())
|
|
1499
|
+
);
|
|
1500
|
+
expiryTimer = setTimeout(() => {
|
|
1501
|
+
expiryTimer = null;
|
|
1502
|
+
void fireSoftExpiry();
|
|
1503
|
+
}, delay);
|
|
1504
|
+
}
|
|
1505
|
+
function noteSoftExpiry(id, at) {
|
|
1506
|
+
if (at === null) return;
|
|
1507
|
+
if (at <= now() && (store.snapshot().conversationSummaries.get(id)?.unreadCount ?? 0) === 0) {
|
|
1508
|
+
return;
|
|
1509
|
+
}
|
|
1510
|
+
const held = pendingExpiry.get(id);
|
|
1511
|
+
if (held !== void 0 && held <= at) return;
|
|
1512
|
+
pendingExpiry.set(id, at);
|
|
1513
|
+
scheduleExpiryTimer();
|
|
1514
|
+
}
|
|
1515
|
+
async function refreshPendingExpiries(ids) {
|
|
1516
|
+
const summaries = store.snapshot().conversationSummaries;
|
|
1517
|
+
const counted = ids.filter((id) => (summaries.get(id)?.unreadCount ?? 0) > 0);
|
|
1518
|
+
ids.forEach((id) => pendingExpiry.delete(id));
|
|
1519
|
+
if (counted.length > 0) {
|
|
1520
|
+
const pending = await repository.pendingSoftExpiries(counted);
|
|
1521
|
+
if (disposed) return;
|
|
1522
|
+
learnServerClock(pending.serverNow);
|
|
1523
|
+
pending.byConversation.forEach(
|
|
1524
|
+
(at, id) => noteSoftExpiry(id, softExpiryTime({ softExpiresAt: at }))
|
|
1525
|
+
);
|
|
1526
|
+
}
|
|
1527
|
+
scheduleExpiryTimer();
|
|
1528
|
+
}
|
|
1529
|
+
async function fireSoftExpiry() {
|
|
1530
|
+
if (disposed) return;
|
|
1531
|
+
const at = now();
|
|
1532
|
+
const due = [...pendingExpiry.entries()].filter(([, expiry]) => expiry <= at).map(([id]) => id);
|
|
1533
|
+
due.forEach((id) => pendingExpiry.delete(id));
|
|
1534
|
+
await Promise.all(
|
|
1535
|
+
due.map(async (id) => {
|
|
1536
|
+
if ((store.snapshot().conversationSummaries.get(id)?.unreadCount ?? 0) === 0) return;
|
|
1537
|
+
try {
|
|
1538
|
+
const count = await repository.unreadCount(id);
|
|
1539
|
+
if (!disposed) store.setUnreadCount(id, count);
|
|
1540
|
+
} catch (error) {
|
|
1541
|
+
reportError(error, "softExpiryUnread", { conversationId: id });
|
|
1542
|
+
}
|
|
1543
|
+
})
|
|
1544
|
+
);
|
|
1545
|
+
if (disposed) return;
|
|
1546
|
+
await refreshPendingExpiries(due).catch(
|
|
1547
|
+
(error) => reportError(error, "softExpiryRefresh")
|
|
1548
|
+
);
|
|
1549
|
+
}
|
|
1550
|
+
function watchSoftExpiries(ids) {
|
|
1551
|
+
void refreshPendingExpiries(ids).catch(
|
|
1552
|
+
(error) => reportError(error, "softExpiryRefresh")
|
|
1553
|
+
);
|
|
1554
|
+
}
|
|
1459
1555
|
const outbox = createOutbox({
|
|
1460
1556
|
...options.outboxStorage !== void 0 ? { storage: options.outboxStorage } : {},
|
|
1461
1557
|
send: (draft, clientMessageId) => repository.insertMessage(draft, clientMessageId),
|
|
@@ -1482,7 +1578,8 @@ function createMessagingEngine(options) {
|
|
|
1482
1578
|
...entry.draft.replyToId !== void 0 ? { replyToId: entry.draft.replyToId } : {},
|
|
1483
1579
|
...entry.draft.action !== void 0 ? { action: entry.draft.action } : {},
|
|
1484
1580
|
...entry.draft.attachments !== void 0 ? { attachments: [...entry.draft.attachments] } : {},
|
|
1485
|
-
...entry.draft.references !== void 0 ? { references: [...entry.draft.references] } : {}
|
|
1581
|
+
...entry.draft.references !== void 0 ? { references: [...entry.draft.references] } : {},
|
|
1582
|
+
...entry.draft.softExpiresAt !== void 0 ? { softExpiresAt: entry.draft.softExpiresAt } : {}
|
|
1486
1583
|
})
|
|
1487
1584
|
);
|
|
1488
1585
|
});
|
|
@@ -1511,6 +1608,7 @@ function createMessagingEngine(options) {
|
|
|
1511
1608
|
if (disposed) return;
|
|
1512
1609
|
conversationCursor = page.nextCursor;
|
|
1513
1610
|
store.setConversations(page.items, page.hasMore);
|
|
1611
|
+
watchSoftExpiries([...store.snapshot().conversationSummaries.keys()]);
|
|
1514
1612
|
}
|
|
1515
1613
|
async function reloadArchivedCount() {
|
|
1516
1614
|
if (disposed) return;
|
|
@@ -1705,6 +1803,7 @@ function createMessagingEngine(options) {
|
|
|
1705
1803
|
store.ingest(message);
|
|
1706
1804
|
const isMine = message.senderId === identity.userId;
|
|
1707
1805
|
store.applyLastMessage(message, { incrementUnread: !isMine });
|
|
1806
|
+
if (!isMine) noteSoftExpiry(message.conversationId, softExpiryTime(message));
|
|
1708
1807
|
if (!isMine && store.snapshot().activeConversationId === message.conversationId) {
|
|
1709
1808
|
void persistRead(message.conversationId).catch(() => void 0);
|
|
1710
1809
|
}
|
|
@@ -1766,6 +1865,7 @@ function createMessagingEngine(options) {
|
|
|
1766
1865
|
});
|
|
1767
1866
|
conversationCursor = page.nextCursor;
|
|
1768
1867
|
store.appendConversations(page.items, page.hasMore);
|
|
1868
|
+
watchSoftExpiries(page.items.map((item) => item.conversation.id));
|
|
1769
1869
|
} catch (error) {
|
|
1770
1870
|
reportError(error, "loadMoreConversations");
|
|
1771
1871
|
}
|
|
@@ -1785,7 +1885,10 @@ function createMessagingEngine(options) {
|
|
|
1785
1885
|
const directSummary = missingSummary ? await repository.getConversationSummary(id) : null;
|
|
1786
1886
|
if (!openIsLive(id, generation, ownedChannel)) return;
|
|
1787
1887
|
store.setConversationReadFailure(id, null);
|
|
1788
|
-
if (directSummary !== null)
|
|
1888
|
+
if (directSummary !== null) {
|
|
1889
|
+
store.setConversationSummary(directSummary);
|
|
1890
|
+
watchSoftExpiries([id]);
|
|
1891
|
+
}
|
|
1789
1892
|
await persistRead(
|
|
1790
1893
|
id,
|
|
1791
1894
|
() => openIsLive(id, generation, ownedChannel)
|
|
@@ -1880,6 +1983,9 @@ function createMessagingEngine(options) {
|
|
|
1880
1983
|
},
|
|
1881
1984
|
dispose() {
|
|
1882
1985
|
disposed = true;
|
|
1986
|
+
if (expiryTimer !== null) clearTimeout(expiryTimer);
|
|
1987
|
+
expiryTimer = null;
|
|
1988
|
+
pendingExpiry.clear();
|
|
1883
1989
|
openChannels.forEach((_handle, id) => {
|
|
1884
1990
|
initialReadGeneration.set(id, (initialReadGeneration.get(id) ?? 0) + 1);
|
|
1885
1991
|
});
|
|
@@ -2141,10 +2247,13 @@ var RPCS = {
|
|
|
2141
2247
|
/** Conversation list + participants + last message + unread, keyset paged. */
|
|
2142
2248
|
conversationsWithDetails: "get_dm_conversations_with_details",
|
|
2143
2249
|
unreadCount: "get_dm_unread_count",
|
|
2250
|
+
/** The soonest future soft expiry per conversation, plus the server's clock. */
|
|
2251
|
+
pendingSoftExpiries: "get_dm_pending_soft_expiries",
|
|
2144
2252
|
userInfo: "get_dm_user_info",
|
|
2145
2253
|
isParticipant: "is_dm_participant"
|
|
2146
2254
|
};
|
|
2147
|
-
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";
|
|
2255
|
+
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,soft_expires_at";
|
|
2256
|
+
var PENDING_SOFT_EXPIRY_CHUNK = 200;
|
|
2148
2257
|
var PARTICIPANT_COLUMNS = "conversation_id,user_id,role,joined_at,last_read_at,is_muted,is_archived";
|
|
2149
2258
|
var CONVERSATION_COLUMNS = "id,type,group_name,group_image_url,created_by,organization_id,created_at,updated_at,metadata";
|
|
2150
2259
|
function optionalOrg(organizationId) {
|
|
@@ -2565,6 +2674,9 @@ function createMessagingRepository(options) {
|
|
|
2565
2674
|
// confirmed row instead of showing the message twice.
|
|
2566
2675
|
client_message_id: clientMessageId,
|
|
2567
2676
|
action_data: draft.action ?? null,
|
|
2677
|
+
// SOFT EXPIRY: written only when the sender sets one. Lapsed is
|
|
2678
|
+
// derived from it live (`isLapsed`), never stamped.
|
|
2679
|
+
...draft.softExpiresAt !== void 0 ? { soft_expires_at: draft.softExpiresAt } : {},
|
|
2568
2680
|
metadata: {
|
|
2569
2681
|
...draft.metadata ?? {},
|
|
2570
2682
|
...draft.attachments !== void 0 && draft.attachments.length > 0 ? { attachments: draft.attachments } : {},
|
|
@@ -2617,6 +2729,56 @@ function createMessagingRepository(options) {
|
|
|
2617
2729
|
}
|
|
2618
2730
|
return Math.floor(unread);
|
|
2619
2731
|
},
|
|
2732
|
+
async unreadCount(conversationId) {
|
|
2733
|
+
const operation = "unreadCount";
|
|
2734
|
+
const me = await actingUser(operation);
|
|
2735
|
+
const unread = await withSessionRetry(
|
|
2736
|
+
operation,
|
|
2737
|
+
() => rpc(
|
|
2738
|
+
RPCS.unreadCount,
|
|
2739
|
+
{ p_conversation_id: conversationId, p_user_id: me },
|
|
2740
|
+
operation
|
|
2741
|
+
)
|
|
2742
|
+
);
|
|
2743
|
+
if (typeof unread !== "number" || !Number.isFinite(unread) || unread < 0) {
|
|
2744
|
+
throw invalidResponse(operation, `${RPCS.unreadCount} returned ${JSON.stringify(unread)}`);
|
|
2745
|
+
}
|
|
2746
|
+
return Math.floor(unread);
|
|
2747
|
+
},
|
|
2748
|
+
async pendingSoftExpiries(conversationIds) {
|
|
2749
|
+
const operation = "pendingSoftExpiries";
|
|
2750
|
+
const unique = [...new Set(conversationIds)];
|
|
2751
|
+
const byConversation = /* @__PURE__ */ new Map();
|
|
2752
|
+
let serverNow = null;
|
|
2753
|
+
for (let start = 0; start < unique.length; start += PENDING_SOFT_EXPIRY_CHUNK) {
|
|
2754
|
+
const chunk = unique.slice(start, start + PENDING_SOFT_EXPIRY_CHUNK);
|
|
2755
|
+
const answer = await withSessionRetry(
|
|
2756
|
+
operation,
|
|
2757
|
+
() => rpc(
|
|
2758
|
+
RPCS.pendingSoftExpiries,
|
|
2759
|
+
{ p_conversation_ids: chunk },
|
|
2760
|
+
operation
|
|
2761
|
+
)
|
|
2762
|
+
);
|
|
2763
|
+
const now = answer?.["server_now"];
|
|
2764
|
+
const pending = answer?.["pending"];
|
|
2765
|
+
if (typeof now !== "string" || !Array.isArray(pending)) {
|
|
2766
|
+
throw invalidResponse(
|
|
2767
|
+
operation,
|
|
2768
|
+
`${RPCS.pendingSoftExpiries} returned ${JSON.stringify(answer)}`
|
|
2769
|
+
);
|
|
2770
|
+
}
|
|
2771
|
+
serverNow = now;
|
|
2772
|
+
for (const row of pending) {
|
|
2773
|
+
const id = row["conversation_id"];
|
|
2774
|
+
const at = row["soft_expires_at"];
|
|
2775
|
+
if (typeof id === "string" && typeof at === "string") {
|
|
2776
|
+
byConversation.set(id, at);
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
return { byConversation, serverNow };
|
|
2781
|
+
},
|
|
2620
2782
|
async setConversationFlags(conversationId, flags) {
|
|
2621
2783
|
const operation = "setConversationFlags";
|
|
2622
2784
|
const me = await actingUser(operation);
|