@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/dist/index.d.cts
CHANGED
|
@@ -134,6 +134,13 @@ interface Message {
|
|
|
134
134
|
readonly attachments: readonly Attachment[];
|
|
135
135
|
readonly references: readonly MatrxReference[];
|
|
136
136
|
readonly metadata: JsonObject;
|
|
137
|
+
/**
|
|
138
|
+
* SOFT EXPIRY (`dm_messages.soft_expires_at`). Past this instant the message
|
|
139
|
+
* is LAPSED: still readable and actionable, never hidden or edited, and it no
|
|
140
|
+
* longer counts as unread. `null` = never lapses. Ask `isLapsed`, never
|
|
141
|
+
* compare this yourself — that is the one lapse rule.
|
|
142
|
+
*/
|
|
143
|
+
readonly softExpiresAt: string | null;
|
|
137
144
|
/** Set only while this message is in the outbox and its last send failed. */
|
|
138
145
|
readonly failureReason?: string | undefined;
|
|
139
146
|
}
|
|
@@ -222,6 +229,8 @@ interface DraftMessage {
|
|
|
222
229
|
readonly attachments?: readonly Attachment[] | undefined;
|
|
223
230
|
readonly references?: readonly MatrxReference[] | undefined;
|
|
224
231
|
readonly metadata?: JsonObject | undefined;
|
|
232
|
+
/** ISO instant after which the message is lapsed (see `Message.softExpiresAt`). */
|
|
233
|
+
readonly softExpiresAt?: string | undefined;
|
|
225
234
|
}
|
|
226
235
|
|
|
227
236
|
/**
|
|
@@ -939,9 +948,22 @@ declare const RPCS: {
|
|
|
939
948
|
/** Conversation list + participants + last message + unread, keyset paged. */
|
|
940
949
|
readonly conversationsWithDetails: "get_dm_conversations_with_details";
|
|
941
950
|
readonly unreadCount: "get_dm_unread_count";
|
|
951
|
+
/** The soonest future soft expiry per conversation, plus the server's clock. */
|
|
952
|
+
readonly pendingSoftExpiries: "get_dm_pending_soft_expiries";
|
|
942
953
|
readonly userInfo: "get_dm_user_info";
|
|
943
954
|
readonly isParticipant: "is_dm_participant";
|
|
944
955
|
};
|
|
956
|
+
/** The answer to `pendingSoftExpiries`: per conversation, and the server's own clock. */
|
|
957
|
+
interface PendingSoftExpiries {
|
|
958
|
+
/** Soonest FUTURE `soft_expires_at` per conversation; absent = nothing pending. */
|
|
959
|
+
readonly byConversation: ReadonlyMap<ConversationId, string>;
|
|
960
|
+
/**
|
|
961
|
+
* The database's `now()` when it answered. The engine measures its clock against this and
|
|
962
|
+
* judges lapse on the SERVER's clock — a skewed browser clock never suppresses a count.
|
|
963
|
+
* `null` when nothing was asked (no conversations), so no clock was read.
|
|
964
|
+
*/
|
|
965
|
+
readonly serverNow: string | null;
|
|
966
|
+
}
|
|
945
967
|
/**
|
|
946
968
|
* The host's session source. A messaging read that lands between sign-in and
|
|
947
969
|
* token arrival must not become a red error — it retries ONCE after the host
|
|
@@ -1054,6 +1076,20 @@ interface MessagingRepository {
|
|
|
1054
1076
|
deleteMessage(id: MessageId, forEveryone: boolean): Promise<void>;
|
|
1055
1077
|
/** Persists the cutoff, then returns the server's remaining unread count. */
|
|
1056
1078
|
markRead(conversationId: ConversationId, at: string): Promise<number>;
|
|
1079
|
+
/**
|
|
1080
|
+
* The server's unread count for one conversation — READ ONLY. It never moves
|
|
1081
|
+
* the watermark. The soft-expiry timer uses it to re-derive unread once a
|
|
1082
|
+
* counted message lapses, instead of guessing with local arithmetic.
|
|
1083
|
+
*/
|
|
1084
|
+
unreadCount(conversationId: ConversationId): Promise<number>;
|
|
1085
|
+
/**
|
|
1086
|
+
* The soonest FUTURE `soft_expires_at` among other people's live messages,
|
|
1087
|
+
* per conversation, for the given conversations, and the server's clock.
|
|
1088
|
+
* Conversations with nothing pending are absent. One call per
|
|
1089
|
+
* `PENDING_SOFT_EXPIRY_CHUNK` conversations, each answering every one of them
|
|
1090
|
+
* — this is what lets ONE timer re-derive unread when a counted message lapses.
|
|
1091
|
+
*/
|
|
1092
|
+
pendingSoftExpiries(conversationIds: readonly ConversationId[]): Promise<PendingSoftExpiries>;
|
|
1057
1093
|
setConversationFlags(conversationId: ConversationId, flags: {
|
|
1058
1094
|
isMuted?: boolean;
|
|
1059
1095
|
isArchived?: boolean;
|
|
@@ -1098,6 +1134,10 @@ declare function createMessagingRepository(options: RepositoryOptions): Messagin
|
|
|
1098
1134
|
* clears unread counts. Only a confirmed participant write may do that, and
|
|
1099
1135
|
* overlapping confirmations apply monotonically so one baseline is never
|
|
1100
1136
|
* subtracted twice.
|
|
1137
|
+
* 6. **A lapsed message never counts as unread** (`isLapsed`, the one lapse
|
|
1138
|
+
* rule). It is ingested and drawn like any other message; it just never
|
|
1139
|
+
* increments a badge. Messages that lapse LATER are re-derived from the
|
|
1140
|
+
* server by the engine's one soft-expiry timer, never subtracted here.
|
|
1101
1141
|
*/
|
|
1102
1142
|
|
|
1103
1143
|
/**
|
|
@@ -1229,7 +1269,11 @@ interface ConversationReadCutoff {
|
|
|
1229
1269
|
/** Per-conversation unread-state revision observed when this read began. */
|
|
1230
1270
|
readonly revision: number;
|
|
1231
1271
|
}
|
|
1232
|
-
|
|
1272
|
+
interface MessagingStoreOptions {
|
|
1273
|
+
/** The clock the lapse rule reads. Defaults to `Date.now`. */
|
|
1274
|
+
now?: (() => number) | undefined;
|
|
1275
|
+
}
|
|
1276
|
+
declare function createMessagingStore(options?: MessagingStoreOptions): MessagingStore;
|
|
1233
1277
|
/**
|
|
1234
1278
|
* The optimistic row a composer shows the instant Enter is pressed. It carries
|
|
1235
1279
|
* the outbox's client key, which is the whole reason the confirmed row can find
|
|
@@ -1246,6 +1290,7 @@ declare function optimisticMessage(args: {
|
|
|
1246
1290
|
action?: Message["action"];
|
|
1247
1291
|
attachments?: readonly Message["attachments"][number][];
|
|
1248
1292
|
references?: readonly Message["references"][number][];
|
|
1293
|
+
softExpiresAt?: string | null;
|
|
1249
1294
|
now?: () => number;
|
|
1250
1295
|
}): Message;
|
|
1251
1296
|
|
|
@@ -1294,6 +1339,8 @@ interface MessagingEngineOptions {
|
|
|
1294
1339
|
* the list.
|
|
1295
1340
|
*/
|
|
1296
1341
|
archiveFilter?: MessagingArchiveFilter | undefined;
|
|
1342
|
+
/** The clock the soft-expiry rule reads. Defaults to `Date.now`. */
|
|
1343
|
+
now?: (() => number) | undefined;
|
|
1297
1344
|
}
|
|
1298
1345
|
interface MessagingEngine {
|
|
1299
1346
|
readonly store: MessagingStore;
|
|
@@ -1384,6 +1431,28 @@ declare function participantNames(participants: readonly UserSummary[], excludin
|
|
|
1384
1431
|
*/
|
|
1385
1432
|
declare function formatLastSeen(lastSeenMs: number | null, now?: number): string;
|
|
1386
1433
|
|
|
1434
|
+
/**
|
|
1435
|
+
* THE LAPSE RULE — the only place in this package that decides whether a
|
|
1436
|
+
* message has soft-expired.
|
|
1437
|
+
*
|
|
1438
|
+
* A message carrying `softExpiresAt` (a meeting invitation is worth reading
|
|
1439
|
+
* until an hour after the meeting starts) is LAPSED once that instant has
|
|
1440
|
+
* passed. Lapsed is DERIVED from the clock every time it is asked, never
|
|
1441
|
+
* stamped on the row: there is no sweeper and nothing to drift. The database's
|
|
1442
|
+
* `get_dm_unread_count` applies the same comparison (`soft_expires_at > now()`
|
|
1443
|
+
* still counts), so the badge the server computes and the bubble the client
|
|
1444
|
+
* draws agree.
|
|
1445
|
+
*
|
|
1446
|
+
* A lapsed message stays readable and its actions keep working. It simply
|
|
1447
|
+
* stops counting as unread — and the reader's watermark is NEVER moved for it,
|
|
1448
|
+
* because moving `last_read_at` would mark every other message read too.
|
|
1449
|
+
*/
|
|
1450
|
+
|
|
1451
|
+
/** Parse a soft-expiry instant; `null` when absent or unparseable (never lapses). */
|
|
1452
|
+
declare function softExpiryTime(message: Pick<Message, "softExpiresAt">): number | null;
|
|
1453
|
+
/** True when the message's soft expiry is at or before `now` (epoch ms or Date). */
|
|
1454
|
+
declare function isLapsed(message: Pick<Message, "softExpiresAt">, now?: number | Date): boolean;
|
|
1455
|
+
|
|
1387
1456
|
/**
|
|
1388
1457
|
* THE INGRESS BOUNDARY — where a database row becomes a domain object.
|
|
1389
1458
|
*
|
|
@@ -1466,4 +1535,4 @@ declare function summarizeText(content: string, maxLength?: number): string;
|
|
|
1466
1535
|
/** Serialize picked references into a fence the platform's other readers accept. */
|
|
1467
1536
|
declare function composeFence(references: readonly MatrxReference[]): string;
|
|
1468
1537
|
|
|
1469
|
-
export { type ActionChoice, type ActionContext, type ActionHandler, type ActionOutcome, type ActionReceipt, type ActionRegistry, type ActorPresentation, type AiCallArgs, type AiCapability, type AiResult, type Attachment, type ClientMessageId, type Conversation, type ConversationAiContext, type ConversationCursor, type ConversationId, type ConversationReadCutoff, type ConversationReadFailure, type ConversationSummary, type ConversationThread, type ConversationType, DEFAULT_MESSAGING_ARCHIVE_FILTER, type DeliveryState, type DraftMessage, type EngineDiagnostic, type JsonObject, type JsonValue, MESSAGING_EVENTS, MESSAGING_RPC_SCHEMA, MESSAGING_SCHEMA, type MatrxReference, type Message, type MessageAction, type MessageCursor, type MessageGroup, type MessageId, type MessageKind, type MessagingAgentIdentity, type MessagingAgents, type MessagingAi, type MessagingAiOptions, type MessagingArchiveFilter, type MessagingEngine, type MessagingEngineOptions, MessagingError, type MessagingErrorCode, type MessagingIdentity, type MessagingRepository, type MessagingSnapshot, type MessagingStore, type MessagingSupabaseClient, type MessagingSupabaseInternal, type OrganizationId, type Outbox, type OutboxEntry, type OutboxOptions, type OutboxStorage, type Page, type Participant, type ParticipantRole, type PostgrestFilterLike, type PostgrestLikeResponse, type PostgrestTableLike, RPCS, type ReadCache, type ReadCacheOptions, type RepositoryOptions, SERVER_SESSION_IDENTITY, type SchemaLike, type SessionIdentity, type SessionIdentitySnapshot, type SessionResolver, type SupabaseLike, TABLES, type TextSegment, type UserId, type UserSummary, asClientMessageId, asConversationId, asMessageId, asOrganizationId, asUserId, composeFence, conversationContextVariables, conversationTopic, createActionRegistry, createMemoryOutboxStorage, createMessagingAi, createMessagingEngine, createMessagingRepository, createMessagingStore, createOutbox, createReadCache, createSessionIdentity, createWebOutboxStorage, effectiveActorKey, extractReferences, formatConversationTime, formatDateSeparator, formatLastSeen, formatMessageTime, formatTypists, groupMessages, inboxTopic, invalidResponse, isSameDay, messagingClientId, normalizeMessagingError, optimisticMessage, participantNames, projectConversationSummary, projectMessage, projectMessageAction, projectParticipantRole, projectUserSummary, resolveActor, splitText, summarizeText, toMessagingArchiveFilter, unreadCutoff, writeNotApplied };
|
|
1538
|
+
export { type ActionChoice, type ActionContext, type ActionHandler, type ActionOutcome, type ActionReceipt, type ActionRegistry, type ActorPresentation, type AiCallArgs, type AiCapability, type AiResult, type Attachment, type ClientMessageId, type Conversation, type ConversationAiContext, type ConversationCursor, type ConversationId, type ConversationReadCutoff, type ConversationReadFailure, type ConversationSummary, type ConversationThread, type ConversationType, DEFAULT_MESSAGING_ARCHIVE_FILTER, type DeliveryState, type DraftMessage, type EngineDiagnostic, type JsonObject, type JsonValue, MESSAGING_EVENTS, MESSAGING_RPC_SCHEMA, MESSAGING_SCHEMA, type MatrxReference, type Message, type MessageAction, type MessageCursor, type MessageGroup, type MessageId, type MessageKind, type MessagingAgentIdentity, type MessagingAgents, type MessagingAi, type MessagingAiOptions, type MessagingArchiveFilter, type MessagingEngine, type MessagingEngineOptions, MessagingError, type MessagingErrorCode, type MessagingIdentity, type MessagingRepository, type MessagingSnapshot, type MessagingStore, type MessagingSupabaseClient, type MessagingSupabaseInternal, type OrganizationId, type Outbox, type OutboxEntry, type OutboxOptions, type OutboxStorage, type Page, type Participant, type ParticipantRole, type PendingSoftExpiries, type PostgrestFilterLike, type PostgrestLikeResponse, type PostgrestTableLike, RPCS, type ReadCache, type ReadCacheOptions, type RepositoryOptions, SERVER_SESSION_IDENTITY, type SchemaLike, type SessionIdentity, type SessionIdentitySnapshot, type SessionResolver, type SupabaseLike, TABLES, type TextSegment, type UserId, type UserSummary, asClientMessageId, asConversationId, asMessageId, asOrganizationId, asUserId, composeFence, conversationContextVariables, conversationTopic, createActionRegistry, createMemoryOutboxStorage, createMessagingAi, createMessagingEngine, createMessagingRepository, createMessagingStore, createOutbox, createReadCache, createSessionIdentity, createWebOutboxStorage, effectiveActorKey, extractReferences, formatConversationTime, formatDateSeparator, formatLastSeen, formatMessageTime, formatTypists, groupMessages, inboxTopic, invalidResponse, isLapsed, isSameDay, messagingClientId, normalizeMessagingError, optimisticMessage, participantNames, projectConversationSummary, projectMessage, projectMessageAction, projectParticipantRole, projectUserSummary, resolveActor, softExpiryTime, splitText, summarizeText, toMessagingArchiveFilter, unreadCutoff, writeNotApplied };
|
package/dist/index.d.ts
CHANGED
|
@@ -134,6 +134,13 @@ interface Message {
|
|
|
134
134
|
readonly attachments: readonly Attachment[];
|
|
135
135
|
readonly references: readonly MatrxReference[];
|
|
136
136
|
readonly metadata: JsonObject;
|
|
137
|
+
/**
|
|
138
|
+
* SOFT EXPIRY (`dm_messages.soft_expires_at`). Past this instant the message
|
|
139
|
+
* is LAPSED: still readable and actionable, never hidden or edited, and it no
|
|
140
|
+
* longer counts as unread. `null` = never lapses. Ask `isLapsed`, never
|
|
141
|
+
* compare this yourself — that is the one lapse rule.
|
|
142
|
+
*/
|
|
143
|
+
readonly softExpiresAt: string | null;
|
|
137
144
|
/** Set only while this message is in the outbox and its last send failed. */
|
|
138
145
|
readonly failureReason?: string | undefined;
|
|
139
146
|
}
|
|
@@ -222,6 +229,8 @@ interface DraftMessage {
|
|
|
222
229
|
readonly attachments?: readonly Attachment[] | undefined;
|
|
223
230
|
readonly references?: readonly MatrxReference[] | undefined;
|
|
224
231
|
readonly metadata?: JsonObject | undefined;
|
|
232
|
+
/** ISO instant after which the message is lapsed (see `Message.softExpiresAt`). */
|
|
233
|
+
readonly softExpiresAt?: string | undefined;
|
|
225
234
|
}
|
|
226
235
|
|
|
227
236
|
/**
|
|
@@ -939,9 +948,22 @@ declare const RPCS: {
|
|
|
939
948
|
/** Conversation list + participants + last message + unread, keyset paged. */
|
|
940
949
|
readonly conversationsWithDetails: "get_dm_conversations_with_details";
|
|
941
950
|
readonly unreadCount: "get_dm_unread_count";
|
|
951
|
+
/** The soonest future soft expiry per conversation, plus the server's clock. */
|
|
952
|
+
readonly pendingSoftExpiries: "get_dm_pending_soft_expiries";
|
|
942
953
|
readonly userInfo: "get_dm_user_info";
|
|
943
954
|
readonly isParticipant: "is_dm_participant";
|
|
944
955
|
};
|
|
956
|
+
/** The answer to `pendingSoftExpiries`: per conversation, and the server's own clock. */
|
|
957
|
+
interface PendingSoftExpiries {
|
|
958
|
+
/** Soonest FUTURE `soft_expires_at` per conversation; absent = nothing pending. */
|
|
959
|
+
readonly byConversation: ReadonlyMap<ConversationId, string>;
|
|
960
|
+
/**
|
|
961
|
+
* The database's `now()` when it answered. The engine measures its clock against this and
|
|
962
|
+
* judges lapse on the SERVER's clock — a skewed browser clock never suppresses a count.
|
|
963
|
+
* `null` when nothing was asked (no conversations), so no clock was read.
|
|
964
|
+
*/
|
|
965
|
+
readonly serverNow: string | null;
|
|
966
|
+
}
|
|
945
967
|
/**
|
|
946
968
|
* The host's session source. A messaging read that lands between sign-in and
|
|
947
969
|
* token arrival must not become a red error — it retries ONCE after the host
|
|
@@ -1054,6 +1076,20 @@ interface MessagingRepository {
|
|
|
1054
1076
|
deleteMessage(id: MessageId, forEveryone: boolean): Promise<void>;
|
|
1055
1077
|
/** Persists the cutoff, then returns the server's remaining unread count. */
|
|
1056
1078
|
markRead(conversationId: ConversationId, at: string): Promise<number>;
|
|
1079
|
+
/**
|
|
1080
|
+
* The server's unread count for one conversation — READ ONLY. It never moves
|
|
1081
|
+
* the watermark. The soft-expiry timer uses it to re-derive unread once a
|
|
1082
|
+
* counted message lapses, instead of guessing with local arithmetic.
|
|
1083
|
+
*/
|
|
1084
|
+
unreadCount(conversationId: ConversationId): Promise<number>;
|
|
1085
|
+
/**
|
|
1086
|
+
* The soonest FUTURE `soft_expires_at` among other people's live messages,
|
|
1087
|
+
* per conversation, for the given conversations, and the server's clock.
|
|
1088
|
+
* Conversations with nothing pending are absent. One call per
|
|
1089
|
+
* `PENDING_SOFT_EXPIRY_CHUNK` conversations, each answering every one of them
|
|
1090
|
+
* — this is what lets ONE timer re-derive unread when a counted message lapses.
|
|
1091
|
+
*/
|
|
1092
|
+
pendingSoftExpiries(conversationIds: readonly ConversationId[]): Promise<PendingSoftExpiries>;
|
|
1057
1093
|
setConversationFlags(conversationId: ConversationId, flags: {
|
|
1058
1094
|
isMuted?: boolean;
|
|
1059
1095
|
isArchived?: boolean;
|
|
@@ -1098,6 +1134,10 @@ declare function createMessagingRepository(options: RepositoryOptions): Messagin
|
|
|
1098
1134
|
* clears unread counts. Only a confirmed participant write may do that, and
|
|
1099
1135
|
* overlapping confirmations apply monotonically so one baseline is never
|
|
1100
1136
|
* subtracted twice.
|
|
1137
|
+
* 6. **A lapsed message never counts as unread** (`isLapsed`, the one lapse
|
|
1138
|
+
* rule). It is ingested and drawn like any other message; it just never
|
|
1139
|
+
* increments a badge. Messages that lapse LATER are re-derived from the
|
|
1140
|
+
* server by the engine's one soft-expiry timer, never subtracted here.
|
|
1101
1141
|
*/
|
|
1102
1142
|
|
|
1103
1143
|
/**
|
|
@@ -1229,7 +1269,11 @@ interface ConversationReadCutoff {
|
|
|
1229
1269
|
/** Per-conversation unread-state revision observed when this read began. */
|
|
1230
1270
|
readonly revision: number;
|
|
1231
1271
|
}
|
|
1232
|
-
|
|
1272
|
+
interface MessagingStoreOptions {
|
|
1273
|
+
/** The clock the lapse rule reads. Defaults to `Date.now`. */
|
|
1274
|
+
now?: (() => number) | undefined;
|
|
1275
|
+
}
|
|
1276
|
+
declare function createMessagingStore(options?: MessagingStoreOptions): MessagingStore;
|
|
1233
1277
|
/**
|
|
1234
1278
|
* The optimistic row a composer shows the instant Enter is pressed. It carries
|
|
1235
1279
|
* the outbox's client key, which is the whole reason the confirmed row can find
|
|
@@ -1246,6 +1290,7 @@ declare function optimisticMessage(args: {
|
|
|
1246
1290
|
action?: Message["action"];
|
|
1247
1291
|
attachments?: readonly Message["attachments"][number][];
|
|
1248
1292
|
references?: readonly Message["references"][number][];
|
|
1293
|
+
softExpiresAt?: string | null;
|
|
1249
1294
|
now?: () => number;
|
|
1250
1295
|
}): Message;
|
|
1251
1296
|
|
|
@@ -1294,6 +1339,8 @@ interface MessagingEngineOptions {
|
|
|
1294
1339
|
* the list.
|
|
1295
1340
|
*/
|
|
1296
1341
|
archiveFilter?: MessagingArchiveFilter | undefined;
|
|
1342
|
+
/** The clock the soft-expiry rule reads. Defaults to `Date.now`. */
|
|
1343
|
+
now?: (() => number) | undefined;
|
|
1297
1344
|
}
|
|
1298
1345
|
interface MessagingEngine {
|
|
1299
1346
|
readonly store: MessagingStore;
|
|
@@ -1384,6 +1431,28 @@ declare function participantNames(participants: readonly UserSummary[], excludin
|
|
|
1384
1431
|
*/
|
|
1385
1432
|
declare function formatLastSeen(lastSeenMs: number | null, now?: number): string;
|
|
1386
1433
|
|
|
1434
|
+
/**
|
|
1435
|
+
* THE LAPSE RULE — the only place in this package that decides whether a
|
|
1436
|
+
* message has soft-expired.
|
|
1437
|
+
*
|
|
1438
|
+
* A message carrying `softExpiresAt` (a meeting invitation is worth reading
|
|
1439
|
+
* until an hour after the meeting starts) is LAPSED once that instant has
|
|
1440
|
+
* passed. Lapsed is DERIVED from the clock every time it is asked, never
|
|
1441
|
+
* stamped on the row: there is no sweeper and nothing to drift. The database's
|
|
1442
|
+
* `get_dm_unread_count` applies the same comparison (`soft_expires_at > now()`
|
|
1443
|
+
* still counts), so the badge the server computes and the bubble the client
|
|
1444
|
+
* draws agree.
|
|
1445
|
+
*
|
|
1446
|
+
* A lapsed message stays readable and its actions keep working. It simply
|
|
1447
|
+
* stops counting as unread — and the reader's watermark is NEVER moved for it,
|
|
1448
|
+
* because moving `last_read_at` would mark every other message read too.
|
|
1449
|
+
*/
|
|
1450
|
+
|
|
1451
|
+
/** Parse a soft-expiry instant; `null` when absent or unparseable (never lapses). */
|
|
1452
|
+
declare function softExpiryTime(message: Pick<Message, "softExpiresAt">): number | null;
|
|
1453
|
+
/** True when the message's soft expiry is at or before `now` (epoch ms or Date). */
|
|
1454
|
+
declare function isLapsed(message: Pick<Message, "softExpiresAt">, now?: number | Date): boolean;
|
|
1455
|
+
|
|
1387
1456
|
/**
|
|
1388
1457
|
* THE INGRESS BOUNDARY — where a database row becomes a domain object.
|
|
1389
1458
|
*
|
|
@@ -1466,4 +1535,4 @@ declare function summarizeText(content: string, maxLength?: number): string;
|
|
|
1466
1535
|
/** Serialize picked references into a fence the platform's other readers accept. */
|
|
1467
1536
|
declare function composeFence(references: readonly MatrxReference[]): string;
|
|
1468
1537
|
|
|
1469
|
-
export { type ActionChoice, type ActionContext, type ActionHandler, type ActionOutcome, type ActionReceipt, type ActionRegistry, type ActorPresentation, type AiCallArgs, type AiCapability, type AiResult, type Attachment, type ClientMessageId, type Conversation, type ConversationAiContext, type ConversationCursor, type ConversationId, type ConversationReadCutoff, type ConversationReadFailure, type ConversationSummary, type ConversationThread, type ConversationType, DEFAULT_MESSAGING_ARCHIVE_FILTER, type DeliveryState, type DraftMessage, type EngineDiagnostic, type JsonObject, type JsonValue, MESSAGING_EVENTS, MESSAGING_RPC_SCHEMA, MESSAGING_SCHEMA, type MatrxReference, type Message, type MessageAction, type MessageCursor, type MessageGroup, type MessageId, type MessageKind, type MessagingAgentIdentity, type MessagingAgents, type MessagingAi, type MessagingAiOptions, type MessagingArchiveFilter, type MessagingEngine, type MessagingEngineOptions, MessagingError, type MessagingErrorCode, type MessagingIdentity, type MessagingRepository, type MessagingSnapshot, type MessagingStore, type MessagingSupabaseClient, type MessagingSupabaseInternal, type OrganizationId, type Outbox, type OutboxEntry, type OutboxOptions, type OutboxStorage, type Page, type Participant, type ParticipantRole, type PostgrestFilterLike, type PostgrestLikeResponse, type PostgrestTableLike, RPCS, type ReadCache, type ReadCacheOptions, type RepositoryOptions, SERVER_SESSION_IDENTITY, type SchemaLike, type SessionIdentity, type SessionIdentitySnapshot, type SessionResolver, type SupabaseLike, TABLES, type TextSegment, type UserId, type UserSummary, asClientMessageId, asConversationId, asMessageId, asOrganizationId, asUserId, composeFence, conversationContextVariables, conversationTopic, createActionRegistry, createMemoryOutboxStorage, createMessagingAi, createMessagingEngine, createMessagingRepository, createMessagingStore, createOutbox, createReadCache, createSessionIdentity, createWebOutboxStorage, effectiveActorKey, extractReferences, formatConversationTime, formatDateSeparator, formatLastSeen, formatMessageTime, formatTypists, groupMessages, inboxTopic, invalidResponse, isSameDay, messagingClientId, normalizeMessagingError, optimisticMessage, participantNames, projectConversationSummary, projectMessage, projectMessageAction, projectParticipantRole, projectUserSummary, resolveActor, splitText, summarizeText, toMessagingArchiveFilter, unreadCutoff, writeNotApplied };
|
|
1538
|
+
export { type ActionChoice, type ActionContext, type ActionHandler, type ActionOutcome, type ActionReceipt, type ActionRegistry, type ActorPresentation, type AiCallArgs, type AiCapability, type AiResult, type Attachment, type ClientMessageId, type Conversation, type ConversationAiContext, type ConversationCursor, type ConversationId, type ConversationReadCutoff, type ConversationReadFailure, type ConversationSummary, type ConversationThread, type ConversationType, DEFAULT_MESSAGING_ARCHIVE_FILTER, type DeliveryState, type DraftMessage, type EngineDiagnostic, type JsonObject, type JsonValue, MESSAGING_EVENTS, MESSAGING_RPC_SCHEMA, MESSAGING_SCHEMA, type MatrxReference, type Message, type MessageAction, type MessageCursor, type MessageGroup, type MessageId, type MessageKind, type MessagingAgentIdentity, type MessagingAgents, type MessagingAi, type MessagingAiOptions, type MessagingArchiveFilter, type MessagingEngine, type MessagingEngineOptions, MessagingError, type MessagingErrorCode, type MessagingIdentity, type MessagingRepository, type MessagingSnapshot, type MessagingStore, type MessagingSupabaseClient, type MessagingSupabaseInternal, type OrganizationId, type Outbox, type OutboxEntry, type OutboxOptions, type OutboxStorage, type Page, type Participant, type ParticipantRole, type PendingSoftExpiries, type PostgrestFilterLike, type PostgrestLikeResponse, type PostgrestTableLike, RPCS, type ReadCache, type ReadCacheOptions, type RepositoryOptions, SERVER_SESSION_IDENTITY, type SchemaLike, type SessionIdentity, type SessionIdentitySnapshot, type SessionResolver, type SupabaseLike, TABLES, type TextSegment, type UserId, type UserSummary, asClientMessageId, asConversationId, asMessageId, asOrganizationId, asUserId, composeFence, conversationContextVariables, conversationTopic, createActionRegistry, createMemoryOutboxStorage, createMessagingAi, createMessagingEngine, createMessagingRepository, createMessagingStore, createOutbox, createReadCache, createSessionIdentity, createWebOutboxStorage, effectiveActorKey, extractReferences, formatConversationTime, formatDateSeparator, formatLastSeen, formatMessageTime, formatTypists, groupMessages, inboxTopic, invalidResponse, isLapsed, isSameDay, messagingClientId, normalizeMessagingError, optimisticMessage, participantNames, projectConversationSummary, projectMessage, projectMessageAction, projectParticipantRole, projectUserSummary, resolveActor, softExpiryTime, splitText, summarizeText, toMessagingArchiveFilter, unreadCutoff, writeNotApplied };
|
package/dist/index.js
CHANGED
|
@@ -901,7 +901,8 @@ function projectBroadcastMessage(value) {
|
|
|
901
901
|
action: projectMessageAction(record["action"]),
|
|
902
902
|
attachments: projectAttachmentList(record["attachments"]),
|
|
903
903
|
references: projectReferenceList(record["references"]),
|
|
904
|
-
metadata
|
|
904
|
+
metadata,
|
|
905
|
+
softExpiresAt: str(record, "softExpiresAt")
|
|
905
906
|
};
|
|
906
907
|
}
|
|
907
908
|
function projectMessage(row, fallbackOrganizationId) {
|
|
@@ -931,7 +932,8 @@ function projectMessage(row, fallbackOrganizationId) {
|
|
|
931
932
|
action,
|
|
932
933
|
attachments: projectAttachments(metadata),
|
|
933
934
|
references: projectReferences(metadata),
|
|
934
|
-
metadata
|
|
935
|
+
metadata,
|
|
936
|
+
softExpiresAt: str(row, "soft_expires_at")
|
|
935
937
|
};
|
|
936
938
|
}
|
|
937
939
|
function projectParticipantRole(value) {
|
|
@@ -984,6 +986,18 @@ function projectConversationSummary(row, viewerId, fallbackOrganizationId) {
|
|
|
984
986
|
};
|
|
985
987
|
}
|
|
986
988
|
|
|
989
|
+
// src/core/lapse.ts
|
|
990
|
+
function softExpiryTime(message) {
|
|
991
|
+
if (message.softExpiresAt === null) return null;
|
|
992
|
+
const parsed = Date.parse(message.softExpiresAt);
|
|
993
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
994
|
+
}
|
|
995
|
+
function isLapsed(message, now = Date.now()) {
|
|
996
|
+
const at = softExpiryTime(message);
|
|
997
|
+
if (at === null) return false;
|
|
998
|
+
return at <= (typeof now === "number" ? now : now.getTime());
|
|
999
|
+
}
|
|
1000
|
+
|
|
987
1001
|
// src/core/types.ts
|
|
988
1002
|
var asConversationId = (value) => value;
|
|
989
1003
|
var asMessageId = (value) => value;
|
|
@@ -1021,7 +1035,8 @@ function insertOrdered(messages, message) {
|
|
|
1021
1035
|
next.splice(index, 0, message);
|
|
1022
1036
|
return next;
|
|
1023
1037
|
}
|
|
1024
|
-
function createMessagingStore() {
|
|
1038
|
+
function createMessagingStore(options = {}) {
|
|
1039
|
+
const now = options.now ?? Date.now;
|
|
1025
1040
|
let conversations = [];
|
|
1026
1041
|
let conversationSummaries = /* @__PURE__ */ new Map();
|
|
1027
1042
|
let hasMoreConversations = false;
|
|
@@ -1258,7 +1273,7 @@ function createMessagingStore() {
|
|
|
1258
1273
|
if (existing === void 0) return;
|
|
1259
1274
|
if (message.deliveryState === "sending" || message.deletedAt !== null) return;
|
|
1260
1275
|
if (existing.lastMessageAt !== null && message.createdAt < existing.lastMessageAt) return;
|
|
1261
|
-
const shouldCount = args.incrementUnread === true;
|
|
1276
|
+
const shouldCount = args.incrementUnread === true && !isLapsed(message, now());
|
|
1262
1277
|
replaceOneSummary(message.conversationId, (item) => ({
|
|
1263
1278
|
...item,
|
|
1264
1279
|
lastMessageContent: message.content,
|
|
@@ -1321,14 +1336,25 @@ function optimisticMessage(args) {
|
|
|
1321
1336
|
action: args.action ?? null,
|
|
1322
1337
|
attachments: args.attachments ?? [],
|
|
1323
1338
|
references: args.references ?? [],
|
|
1324
|
-
metadata: {}
|
|
1339
|
+
metadata: {},
|
|
1340
|
+
softExpiresAt: args.softExpiresAt ?? null
|
|
1325
1341
|
};
|
|
1326
1342
|
}
|
|
1327
1343
|
|
|
1328
1344
|
// src/core/engine.ts
|
|
1345
|
+
var SOFT_EXPIRY_GRACE_MS = 2e3;
|
|
1346
|
+
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
1329
1347
|
function createMessagingEngine(options) {
|
|
1330
1348
|
const { repository, manager, identity } = options;
|
|
1331
|
-
const
|
|
1349
|
+
const browserNow = options.now ?? Date.now;
|
|
1350
|
+
let serverClockOffsetMs = 0;
|
|
1351
|
+
const now = () => browserNow() + serverClockOffsetMs;
|
|
1352
|
+
function learnServerClock(serverNow) {
|
|
1353
|
+
if (serverNow === null) return;
|
|
1354
|
+
const server = Date.parse(serverNow);
|
|
1355
|
+
if (Number.isFinite(server)) serverClockOffsetMs = server - browserNow();
|
|
1356
|
+
}
|
|
1357
|
+
const store = createMessagingStore({ now });
|
|
1332
1358
|
const conversationPageSize = options.conversationPageSize ?? 30;
|
|
1333
1359
|
const messagePageSize = options.messagePageSize ?? 50;
|
|
1334
1360
|
const openChannels = /* @__PURE__ */ new Map();
|
|
@@ -1342,6 +1368,8 @@ function createMessagingEngine(options) {
|
|
|
1342
1368
|
let inboxChannel = null;
|
|
1343
1369
|
let conversationCursor = null;
|
|
1344
1370
|
let disposed = false;
|
|
1371
|
+
const pendingExpiry = /* @__PURE__ */ new Map();
|
|
1372
|
+
let expiryTimer = null;
|
|
1345
1373
|
function report(event) {
|
|
1346
1374
|
options.onDiagnostic?.(event);
|
|
1347
1375
|
}
|
|
@@ -1384,6 +1412,72 @@ function createMessagingEngine(options) {
|
|
|
1384
1412
|
}).catch(() => void 0);
|
|
1385
1413
|
return current;
|
|
1386
1414
|
}
|
|
1415
|
+
function scheduleExpiryTimer() {
|
|
1416
|
+
if (expiryTimer !== null) {
|
|
1417
|
+
clearTimeout(expiryTimer);
|
|
1418
|
+
expiryTimer = null;
|
|
1419
|
+
}
|
|
1420
|
+
if (disposed || pendingExpiry.size === 0) return;
|
|
1421
|
+
const soonest = Math.min(...pendingExpiry.values());
|
|
1422
|
+
const delay = Math.min(
|
|
1423
|
+
MAX_TIMER_DELAY_MS,
|
|
1424
|
+
Math.max(0, soonest + SOFT_EXPIRY_GRACE_MS - now())
|
|
1425
|
+
);
|
|
1426
|
+
expiryTimer = setTimeout(() => {
|
|
1427
|
+
expiryTimer = null;
|
|
1428
|
+
void fireSoftExpiry();
|
|
1429
|
+
}, delay);
|
|
1430
|
+
}
|
|
1431
|
+
function noteSoftExpiry(id, at) {
|
|
1432
|
+
if (at === null) return;
|
|
1433
|
+
if (at <= now() && (store.snapshot().conversationSummaries.get(id)?.unreadCount ?? 0) === 0) {
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
const held = pendingExpiry.get(id);
|
|
1437
|
+
if (held !== void 0 && held <= at) return;
|
|
1438
|
+
pendingExpiry.set(id, at);
|
|
1439
|
+
scheduleExpiryTimer();
|
|
1440
|
+
}
|
|
1441
|
+
async function refreshPendingExpiries(ids) {
|
|
1442
|
+
const summaries = store.snapshot().conversationSummaries;
|
|
1443
|
+
const counted = ids.filter((id) => (summaries.get(id)?.unreadCount ?? 0) > 0);
|
|
1444
|
+
ids.forEach((id) => pendingExpiry.delete(id));
|
|
1445
|
+
if (counted.length > 0) {
|
|
1446
|
+
const pending = await repository.pendingSoftExpiries(counted);
|
|
1447
|
+
if (disposed) return;
|
|
1448
|
+
learnServerClock(pending.serverNow);
|
|
1449
|
+
pending.byConversation.forEach(
|
|
1450
|
+
(at, id) => noteSoftExpiry(id, softExpiryTime({ softExpiresAt: at }))
|
|
1451
|
+
);
|
|
1452
|
+
}
|
|
1453
|
+
scheduleExpiryTimer();
|
|
1454
|
+
}
|
|
1455
|
+
async function fireSoftExpiry() {
|
|
1456
|
+
if (disposed) return;
|
|
1457
|
+
const at = now();
|
|
1458
|
+
const due = [...pendingExpiry.entries()].filter(([, expiry]) => expiry <= at).map(([id]) => id);
|
|
1459
|
+
due.forEach((id) => pendingExpiry.delete(id));
|
|
1460
|
+
await Promise.all(
|
|
1461
|
+
due.map(async (id) => {
|
|
1462
|
+
if ((store.snapshot().conversationSummaries.get(id)?.unreadCount ?? 0) === 0) return;
|
|
1463
|
+
try {
|
|
1464
|
+
const count = await repository.unreadCount(id);
|
|
1465
|
+
if (!disposed) store.setUnreadCount(id, count);
|
|
1466
|
+
} catch (error) {
|
|
1467
|
+
reportError(error, "softExpiryUnread", { conversationId: id });
|
|
1468
|
+
}
|
|
1469
|
+
})
|
|
1470
|
+
);
|
|
1471
|
+
if (disposed) return;
|
|
1472
|
+
await refreshPendingExpiries(due).catch(
|
|
1473
|
+
(error) => reportError(error, "softExpiryRefresh")
|
|
1474
|
+
);
|
|
1475
|
+
}
|
|
1476
|
+
function watchSoftExpiries(ids) {
|
|
1477
|
+
void refreshPendingExpiries(ids).catch(
|
|
1478
|
+
(error) => reportError(error, "softExpiryRefresh")
|
|
1479
|
+
);
|
|
1480
|
+
}
|
|
1387
1481
|
const outbox = createOutbox({
|
|
1388
1482
|
...options.outboxStorage !== void 0 ? { storage: options.outboxStorage } : {},
|
|
1389
1483
|
send: (draft, clientMessageId) => repository.insertMessage(draft, clientMessageId),
|
|
@@ -1410,7 +1504,8 @@ function createMessagingEngine(options) {
|
|
|
1410
1504
|
...entry.draft.replyToId !== void 0 ? { replyToId: entry.draft.replyToId } : {},
|
|
1411
1505
|
...entry.draft.action !== void 0 ? { action: entry.draft.action } : {},
|
|
1412
1506
|
...entry.draft.attachments !== void 0 ? { attachments: [...entry.draft.attachments] } : {},
|
|
1413
|
-
...entry.draft.references !== void 0 ? { references: [...entry.draft.references] } : {}
|
|
1507
|
+
...entry.draft.references !== void 0 ? { references: [...entry.draft.references] } : {},
|
|
1508
|
+
...entry.draft.softExpiresAt !== void 0 ? { softExpiresAt: entry.draft.softExpiresAt } : {}
|
|
1414
1509
|
})
|
|
1415
1510
|
);
|
|
1416
1511
|
});
|
|
@@ -1439,6 +1534,7 @@ function createMessagingEngine(options) {
|
|
|
1439
1534
|
if (disposed) return;
|
|
1440
1535
|
conversationCursor = page.nextCursor;
|
|
1441
1536
|
store.setConversations(page.items, page.hasMore);
|
|
1537
|
+
watchSoftExpiries([...store.snapshot().conversationSummaries.keys()]);
|
|
1442
1538
|
}
|
|
1443
1539
|
async function reloadArchivedCount() {
|
|
1444
1540
|
if (disposed) return;
|
|
@@ -1633,6 +1729,7 @@ function createMessagingEngine(options) {
|
|
|
1633
1729
|
store.ingest(message);
|
|
1634
1730
|
const isMine = message.senderId === identity.userId;
|
|
1635
1731
|
store.applyLastMessage(message, { incrementUnread: !isMine });
|
|
1732
|
+
if (!isMine) noteSoftExpiry(message.conversationId, softExpiryTime(message));
|
|
1636
1733
|
if (!isMine && store.snapshot().activeConversationId === message.conversationId) {
|
|
1637
1734
|
void persistRead(message.conversationId).catch(() => void 0);
|
|
1638
1735
|
}
|
|
@@ -1694,6 +1791,7 @@ function createMessagingEngine(options) {
|
|
|
1694
1791
|
});
|
|
1695
1792
|
conversationCursor = page.nextCursor;
|
|
1696
1793
|
store.appendConversations(page.items, page.hasMore);
|
|
1794
|
+
watchSoftExpiries(page.items.map((item) => item.conversation.id));
|
|
1697
1795
|
} catch (error) {
|
|
1698
1796
|
reportError(error, "loadMoreConversations");
|
|
1699
1797
|
}
|
|
@@ -1713,7 +1811,10 @@ function createMessagingEngine(options) {
|
|
|
1713
1811
|
const directSummary = missingSummary ? await repository.getConversationSummary(id) : null;
|
|
1714
1812
|
if (!openIsLive(id, generation, ownedChannel)) return;
|
|
1715
1813
|
store.setConversationReadFailure(id, null);
|
|
1716
|
-
if (directSummary !== null)
|
|
1814
|
+
if (directSummary !== null) {
|
|
1815
|
+
store.setConversationSummary(directSummary);
|
|
1816
|
+
watchSoftExpiries([id]);
|
|
1817
|
+
}
|
|
1717
1818
|
await persistRead(
|
|
1718
1819
|
id,
|
|
1719
1820
|
() => openIsLive(id, generation, ownedChannel)
|
|
@@ -1808,6 +1909,9 @@ function createMessagingEngine(options) {
|
|
|
1808
1909
|
},
|
|
1809
1910
|
dispose() {
|
|
1810
1911
|
disposed = true;
|
|
1912
|
+
if (expiryTimer !== null) clearTimeout(expiryTimer);
|
|
1913
|
+
expiryTimer = null;
|
|
1914
|
+
pendingExpiry.clear();
|
|
1811
1915
|
openChannels.forEach((_handle, id) => {
|
|
1812
1916
|
initialReadGeneration.set(id, (initialReadGeneration.get(id) ?? 0) + 1);
|
|
1813
1917
|
});
|
|
@@ -2069,10 +2173,13 @@ var RPCS = {
|
|
|
2069
2173
|
/** Conversation list + participants + last message + unread, keyset paged. */
|
|
2070
2174
|
conversationsWithDetails: "get_dm_conversations_with_details",
|
|
2071
2175
|
unreadCount: "get_dm_unread_count",
|
|
2176
|
+
/** The soonest future soft expiry per conversation, plus the server's clock. */
|
|
2177
|
+
pendingSoftExpiries: "get_dm_pending_soft_expiries",
|
|
2072
2178
|
userInfo: "get_dm_user_info",
|
|
2073
2179
|
isParticipant: "is_dm_participant"
|
|
2074
2180
|
};
|
|
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";
|
|
2181
|
+
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";
|
|
2182
|
+
var PENDING_SOFT_EXPIRY_CHUNK = 200;
|
|
2076
2183
|
var PARTICIPANT_COLUMNS = "conversation_id,user_id,role,joined_at,last_read_at,is_muted,is_archived";
|
|
2077
2184
|
var CONVERSATION_COLUMNS = "id,type,group_name,group_image_url,created_by,organization_id,created_at,updated_at,metadata";
|
|
2078
2185
|
function optionalOrg(organizationId) {
|
|
@@ -2493,6 +2600,9 @@ function createMessagingRepository(options) {
|
|
|
2493
2600
|
// confirmed row instead of showing the message twice.
|
|
2494
2601
|
client_message_id: clientMessageId,
|
|
2495
2602
|
action_data: draft.action ?? null,
|
|
2603
|
+
// SOFT EXPIRY: written only when the sender sets one. Lapsed is
|
|
2604
|
+
// derived from it live (`isLapsed`), never stamped.
|
|
2605
|
+
...draft.softExpiresAt !== void 0 ? { soft_expires_at: draft.softExpiresAt } : {},
|
|
2496
2606
|
metadata: {
|
|
2497
2607
|
...draft.metadata ?? {},
|
|
2498
2608
|
...draft.attachments !== void 0 && draft.attachments.length > 0 ? { attachments: draft.attachments } : {},
|
|
@@ -2545,6 +2655,56 @@ function createMessagingRepository(options) {
|
|
|
2545
2655
|
}
|
|
2546
2656
|
return Math.floor(unread);
|
|
2547
2657
|
},
|
|
2658
|
+
async unreadCount(conversationId) {
|
|
2659
|
+
const operation = "unreadCount";
|
|
2660
|
+
const me = await actingUser(operation);
|
|
2661
|
+
const unread = await withSessionRetry(
|
|
2662
|
+
operation,
|
|
2663
|
+
() => rpc(
|
|
2664
|
+
RPCS.unreadCount,
|
|
2665
|
+
{ p_conversation_id: conversationId, p_user_id: me },
|
|
2666
|
+
operation
|
|
2667
|
+
)
|
|
2668
|
+
);
|
|
2669
|
+
if (typeof unread !== "number" || !Number.isFinite(unread) || unread < 0) {
|
|
2670
|
+
throw invalidResponse(operation, `${RPCS.unreadCount} returned ${JSON.stringify(unread)}`);
|
|
2671
|
+
}
|
|
2672
|
+
return Math.floor(unread);
|
|
2673
|
+
},
|
|
2674
|
+
async pendingSoftExpiries(conversationIds) {
|
|
2675
|
+
const operation = "pendingSoftExpiries";
|
|
2676
|
+
const unique = [...new Set(conversationIds)];
|
|
2677
|
+
const byConversation = /* @__PURE__ */ new Map();
|
|
2678
|
+
let serverNow = null;
|
|
2679
|
+
for (let start = 0; start < unique.length; start += PENDING_SOFT_EXPIRY_CHUNK) {
|
|
2680
|
+
const chunk = unique.slice(start, start + PENDING_SOFT_EXPIRY_CHUNK);
|
|
2681
|
+
const answer = await withSessionRetry(
|
|
2682
|
+
operation,
|
|
2683
|
+
() => rpc(
|
|
2684
|
+
RPCS.pendingSoftExpiries,
|
|
2685
|
+
{ p_conversation_ids: chunk },
|
|
2686
|
+
operation
|
|
2687
|
+
)
|
|
2688
|
+
);
|
|
2689
|
+
const now = answer?.["server_now"];
|
|
2690
|
+
const pending = answer?.["pending"];
|
|
2691
|
+
if (typeof now !== "string" || !Array.isArray(pending)) {
|
|
2692
|
+
throw invalidResponse(
|
|
2693
|
+
operation,
|
|
2694
|
+
`${RPCS.pendingSoftExpiries} returned ${JSON.stringify(answer)}`
|
|
2695
|
+
);
|
|
2696
|
+
}
|
|
2697
|
+
serverNow = now;
|
|
2698
|
+
for (const row of pending) {
|
|
2699
|
+
const id = row["conversation_id"];
|
|
2700
|
+
const at = row["soft_expires_at"];
|
|
2701
|
+
if (typeof id === "string" && typeof at === "string") {
|
|
2702
|
+
byConversation.set(id, at);
|
|
2703
|
+
}
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
return { byConversation, serverNow };
|
|
2707
|
+
},
|
|
2548
2708
|
async setConversationFlags(conversationId, flags) {
|
|
2549
2709
|
const operation = "setConversationFlags";
|
|
2550
2710
|
const me = await actingUser(operation);
|
|
@@ -2657,6 +2817,7 @@ export {
|
|
|
2657
2817
|
groupMessages,
|
|
2658
2818
|
inboxTopic,
|
|
2659
2819
|
invalidResponse,
|
|
2820
|
+
isLapsed,
|
|
2660
2821
|
isSameDay,
|
|
2661
2822
|
messagingClientId,
|
|
2662
2823
|
normalizeMessagingError,
|
|
@@ -2668,6 +2829,7 @@ export {
|
|
|
2668
2829
|
projectParticipantRole,
|
|
2669
2830
|
projectUserSummary,
|
|
2670
2831
|
resolveActor,
|
|
2832
|
+
softExpiryTime,
|
|
2671
2833
|
splitText,
|
|
2672
2834
|
summarizeText,
|
|
2673
2835
|
toMessagingArchiveFilter,
|