@openmarket/rooms-client 0.3.0 → 0.4.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/dist/client.d.ts +24 -4
- package/dist/client.js +41 -8
- package/dist/shared/rooms-protocol.d.ts +29 -0
- package/dist/shared/rooms-protocol.js +1 -0
- package/dist/social-types.d.ts +3 -0
- package/dist/types.d.ts +1 -1
- package/package.json +1 -1
- package/src/client.ts +51 -5
- package/src/shared/rooms-protocol.ts +35 -1
- package/src/social-types.ts +3 -0
- package/src/types.ts +2 -0
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type RoomAccessMode, type RoomAttachmentInput, type RoomAuditLogPayload, type RoomAuthSuccessPayload, type RoomBackfillPayload, type RoomDmHistoryResultPayload, type RoomDmMessagePayload, type RoomDmOpenedPayload, type RoomFriendPresence, type RoomInvitedPayload, type RoomLedgerEntry, type RoomMemberRemovedPayload, type RoomMemberRoleChangedPayload, type RoomMeta, type RoomPinsPayload, type RoomPresencePayload, type RoomReactorsPayload, type RoomReadStatePayload, type RoomReportedPayload, type RoomRole, type RoomSearchResultsPayload, type RoomSearchScope, type RoomSpace, type RoomThreadPayload, type RoomTopic, type RoomTopicAttention, type RoomTopicResultPayload, type RoomTopicsResultPayload, type RoomUserStatus, type RoomWhoPayload } from "./types.js";
|
|
1
|
+
import { type RoomAccessMode, type RoomAttachmentInput, type RoomAuditLogPayload, type RoomAuthSuccessPayload, type RoomBackfillPayload, type RoomClientPlatform, type RoomDmHistoryResultPayload, type RoomDmMessagePayload, type RoomDmOpenedPayload, type RoomFriendPresence, type RoomInvitedPayload, type RoomLedgerEntry, type RoomMemberRemovedPayload, type RoomMemberRoleChangedPayload, type RoomMeta, type RoomPinsPayload, type RoomPresencePayload, type RoomReactorsPayload, type RoomReadStatePayload, type RoomReportedPayload, type RoomRole, type RoomSearchResultsPayload, type RoomSearchScope, type RoomSpace, type RoomThreadPayload, type RoomTopic, type RoomTopicAttention, type RoomTopicResultPayload, type RoomTopicsResultPayload, type RoomUserStatus, type RoomWhoPayload } from "./types.js";
|
|
2
2
|
import { type RoomWebSocketCtor, RoomWsClient } from "./ws-client.js";
|
|
3
3
|
export interface RoomClientConfig {
|
|
4
4
|
url: string;
|
|
@@ -283,9 +283,18 @@ export declare function joinRoom(config: RoomClientConfig, options: RoomJoinOpti
|
|
|
283
283
|
/** Re-join a room on an already-connected client (used after a reconnect). */
|
|
284
284
|
export declare function rejoinRoomOnClient(client: RoomWsClient, options: RoomJoinOptions): Promise<RoomBackfillPayload>;
|
|
285
285
|
export declare function attachRejoinOnReconnect(client: RoomWsClient, options: RoomJoinOptions, latestSeq: () => number | undefined, onRejoined?: (backfill: RoomBackfillPayload) => void, onError?: (err: unknown) => void): () => void;
|
|
286
|
-
|
|
286
|
+
/**
|
|
287
|
+
* Post into a joined room and return the frame's clientMsgId (what MESSAGE
|
|
288
|
+
* echoes carry back for optimistic-send reconciliation). `clientMsgId` is
|
|
289
|
+
* caller-suppliable so an offline send queue can retry one logical message
|
|
290
|
+
* under one stable id (same contract as sayRoom / sendRoomDmOnClient);
|
|
291
|
+
* omitted, it mints a UUID exactly as before. The relay validates the id
|
|
292
|
+
* (1..128 chars after trim); dedupe on (userId, clientMsgId) is server-side
|
|
293
|
+
* work and NOT implied here.
|
|
294
|
+
*/
|
|
295
|
+
export declare function postRoomMessage(client: RoomWsClient, room: string, text: string, inReplyTo?: number, attachments?: RoomAttachmentInput[], topicId?: string, clientMsgId?: string): string;
|
|
287
296
|
export declare function sendRoomTyping(client: RoomWsClient, room: string, state: "start" | "stop"): void;
|
|
288
|
-
export declare function sendRoomStatus(client: RoomWsClient, status: RoomUserStatus, statusText?: string | null): void;
|
|
297
|
+
export declare function sendRoomStatus(client: RoomWsClient, status: RoomUserStatus, statusText?: string | null, platform?: RoomClientPlatform): void;
|
|
289
298
|
/**
|
|
290
299
|
* Fetch a joined room's meta (title, access, postingRoles, viewerRole) over
|
|
291
300
|
* the live socket via a scoped SEARCH. Returns null on timeout/no match so
|
|
@@ -310,7 +319,18 @@ export declare function requestRoomReactors(client: RoomWsClient, request: {
|
|
|
310
319
|
* persists it durably; a one-shot socket has no joined rooms, so live
|
|
311
320
|
* rosters pick the change up from the store (the TUI/GUI send STATUS on
|
|
312
321
|
* their live connection instead, which also broadcasts PRESENCE_DELTA). */
|
|
313
|
-
export declare function setRoomStatusOnce(config: RoomClientConfig, status: RoomUserStatus, statusText?: string | null): Promise<void>;
|
|
322
|
+
export declare function setRoomStatusOnce(config: RoomClientConfig, status: RoomUserStatus, statusText?: string | null, platform?: RoomClientPlatform): Promise<void>;
|
|
323
|
+
/**
|
|
324
|
+
* Set the caller's room-level attention (notification posture) on a live
|
|
325
|
+
* client. The relay validates room participation, persists through the
|
|
326
|
+
* store, and acks with a READ_STATE frame scoped to this room whose
|
|
327
|
+
* attention map carries the persisted value (readState stays empty: cursors
|
|
328
|
+
* are untouched). Private per-user state; nothing broadcasts.
|
|
329
|
+
*/
|
|
330
|
+
export declare function setRoomAttentionOnClient(client: RoomWsClient, request: {
|
|
331
|
+
room: string;
|
|
332
|
+
attention: RoomTopicAttention;
|
|
333
|
+
}): Promise<RoomReadStatePayload>;
|
|
314
334
|
/** Add/remove one reaction (one-shot). Returns the updated entry. */
|
|
315
335
|
export declare function reactRoom(config: RoomClientConfig, options: {
|
|
316
336
|
room: string;
|
package/dist/client.js
CHANGED
|
@@ -618,21 +618,30 @@ export function attachRejoinOnReconnect(client, options, latestSeq, onRejoined,
|
|
|
618
618
|
.catch((err) => onError?.(err));
|
|
619
619
|
});
|
|
620
620
|
}
|
|
621
|
-
|
|
622
|
-
|
|
621
|
+
/**
|
|
622
|
+
* Post into a joined room and return the frame's clientMsgId (what MESSAGE
|
|
623
|
+
* echoes carry back for optimistic-send reconciliation). `clientMsgId` is
|
|
624
|
+
* caller-suppliable so an offline send queue can retry one logical message
|
|
625
|
+
* under one stable id (same contract as sayRoom / sendRoomDmOnClient);
|
|
626
|
+
* omitted, it mints a UUID exactly as before. The relay validates the id
|
|
627
|
+
* (1..128 chars after trim); dedupe on (userId, clientMsgId) is server-side
|
|
628
|
+
* work and NOT implied here.
|
|
629
|
+
*/
|
|
630
|
+
export function postRoomMessage(client, room, text, inReplyTo, attachments, topicId, clientMsgId) {
|
|
631
|
+
const msgId = clientMsgId ?? randomUUID();
|
|
623
632
|
client.send({
|
|
624
633
|
type: RoomClientMessageType.POST,
|
|
625
634
|
payload: {
|
|
626
635
|
room,
|
|
627
636
|
...(text ? { text } : {}),
|
|
628
637
|
...(attachments?.length ? { attachments: toRoomAttachmentPayloads(attachments) } : {}),
|
|
629
|
-
clientMsgId,
|
|
638
|
+
clientMsgId: msgId,
|
|
630
639
|
...(inReplyTo !== undefined ? { inReplyTo } : {}),
|
|
631
640
|
...(topicId ? { topicId } : {}),
|
|
632
641
|
},
|
|
633
642
|
timestamp: Date.now(),
|
|
634
643
|
});
|
|
635
|
-
return
|
|
644
|
+
return msgId;
|
|
636
645
|
}
|
|
637
646
|
export function sendRoomTyping(client, room, state) {
|
|
638
647
|
if (client.isGuest())
|
|
@@ -643,10 +652,16 @@ export function sendRoomTyping(client, room, state) {
|
|
|
643
652
|
timestamp: Date.now(),
|
|
644
653
|
});
|
|
645
654
|
}
|
|
646
|
-
export function sendRoomStatus(client, status, statusText) {
|
|
655
|
+
export function sendRoomStatus(client, status, statusText, platform) {
|
|
647
656
|
client.send({
|
|
648
657
|
type: RoomClientMessageType.STATUS,
|
|
649
|
-
payload: {
|
|
658
|
+
payload: {
|
|
659
|
+
status,
|
|
660
|
+
...(statusText !== undefined ? { statusText } : {}),
|
|
661
|
+
// Session identity, declared once per connection (or on change): the
|
|
662
|
+
// relay carries it onto presence member entries.
|
|
663
|
+
...(platform ? { platform } : {}),
|
|
664
|
+
},
|
|
650
665
|
timestamp: Date.now(),
|
|
651
666
|
});
|
|
652
667
|
}
|
|
@@ -769,11 +784,11 @@ export async function requestRoomReactors(client, request) {
|
|
|
769
784
|
* persists it durably; a one-shot socket has no joined rooms, so live
|
|
770
785
|
* rosters pick the change up from the store (the TUI/GUI send STATUS on
|
|
771
786
|
* their live connection instead, which also broadcasts PRESENCE_DELTA). */
|
|
772
|
-
export async function setRoomStatusOnce(config, status, statusText) {
|
|
787
|
+
export async function setRoomStatusOnce(config, status, statusText, platform) {
|
|
773
788
|
const client = new RoomWsClient(config);
|
|
774
789
|
try {
|
|
775
790
|
await client.connect();
|
|
776
|
-
sendRoomStatus(client, status, statusText);
|
|
791
|
+
sendRoomStatus(client, status, statusText, platform);
|
|
777
792
|
// STATUS has no ack frame: give the socket a beat to flush before close.
|
|
778
793
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
779
794
|
}
|
|
@@ -781,6 +796,24 @@ export async function setRoomStatusOnce(config, status, statusText) {
|
|
|
781
796
|
client.close();
|
|
782
797
|
}
|
|
783
798
|
}
|
|
799
|
+
/**
|
|
800
|
+
* Set the caller's room-level attention (notification posture) on a live
|
|
801
|
+
* client. The relay validates room participation, persists through the
|
|
802
|
+
* store, and acks with a READ_STATE frame scoped to this room whose
|
|
803
|
+
* attention map carries the persisted value (readState stays empty: cursors
|
|
804
|
+
* are untouched). Private per-user state; nothing broadcasts.
|
|
805
|
+
*/
|
|
806
|
+
export async function setRoomAttentionOnClient(client, request) {
|
|
807
|
+
const requestId = randomUUID();
|
|
808
|
+
const room = normalizeRoomName(request.room);
|
|
809
|
+
client.send({
|
|
810
|
+
type: RoomClientMessageType.ROOM_SET_ATTENTION,
|
|
811
|
+
payload: { requestId, room, attention: request.attention },
|
|
812
|
+
timestamp: Date.now(),
|
|
813
|
+
});
|
|
814
|
+
const result = await client.waitFor(RoomServerMessageType.READ_STATE, (msg) => msg.payload.requestId === requestId, undefined, requestId);
|
|
815
|
+
return result.payload;
|
|
816
|
+
}
|
|
784
817
|
/** One-shot session helper for the config-based mutation verbs below (the
|
|
785
818
|
* agent tools' transport: connect, join, act, wait for the echo, close). */
|
|
786
819
|
async function withRoomEcho(config, room, seq, send) {
|
|
@@ -48,6 +48,7 @@ export declare const RoomClientMessageType: {
|
|
|
48
48
|
readonly TOPIC_RESOLVE: "TOPIC_RESOLVE";
|
|
49
49
|
readonly TOPIC_SET_ATTENTION: "TOPIC_SET_ATTENTION";
|
|
50
50
|
readonly TOPIC_LIST: "TOPIC_LIST";
|
|
51
|
+
readonly ROOM_SET_ATTENTION: "ROOM_SET_ATTENTION";
|
|
51
52
|
};
|
|
52
53
|
export type RoomClientMessageType = (typeof RoomClientMessageType)[keyof typeof RoomClientMessageType];
|
|
53
54
|
export declare const RoomServerMessageType: {
|
|
@@ -127,6 +128,9 @@ export type RoomRole = "owner" | "admin" | "mod" | "member";
|
|
|
127
128
|
* semantics of an absent value). */
|
|
128
129
|
export type RoomTopicsPolicy = "off" | "allowed" | "required";
|
|
129
130
|
export type RoomTopicAttention = "follow" | "default" | "mute";
|
|
131
|
+
/** Self-declared client class on STATUS frames; presence member entries
|
|
132
|
+
* carry it through so rosters can render where someone is connected from. */
|
|
133
|
+
export type RoomClientPlatform = "web" | "desktop" | "mobile" | "tui";
|
|
130
134
|
export interface RoomMember {
|
|
131
135
|
userId: string;
|
|
132
136
|
username: string;
|
|
@@ -140,6 +144,8 @@ export interface RoomMember {
|
|
|
140
144
|
joinedAt: number;
|
|
141
145
|
avatarUrl?: string | null;
|
|
142
146
|
statusText?: string | null;
|
|
147
|
+
/** Present when the member's session declared one via STATUS. */
|
|
148
|
+
platform?: RoomClientPlatform | undefined;
|
|
143
149
|
}
|
|
144
150
|
export interface RoomMeta {
|
|
145
151
|
name: string;
|
|
@@ -164,6 +170,10 @@ export interface RoomMeta {
|
|
|
164
170
|
* 2026-07-12 dial-default flip (post-flip relays always advertise the
|
|
165
171
|
* stored value, "off" included); explicit "off" means topics are inert. */
|
|
166
172
|
topicsPolicy?: RoomTopicsPolicy | undefined;
|
|
173
|
+
/** The viewer's room attention (notification posture), a per-viewer field
|
|
174
|
+
* like viewerRole: surfaced on rooms listings for the caller. Absent
|
|
175
|
+
* reads as "default". */
|
|
176
|
+
attention?: RoomTopicAttention | undefined;
|
|
167
177
|
tags?: string[] | undefined;
|
|
168
178
|
entities?: string[] | undefined;
|
|
169
179
|
sourceRefs?: Array<Record<string, unknown>> | undefined;
|
|
@@ -295,7 +305,12 @@ export interface RoomPinsPayload {
|
|
|
295
305
|
requestId?: string | undefined;
|
|
296
306
|
}
|
|
297
307
|
export interface RoomReadStatePayload {
|
|
308
|
+
/** Partial map: rooms absent from a reply are unchanged. */
|
|
298
309
|
readState: Record<string, number>;
|
|
310
|
+
/** Per-room attention for the requesting user, keyed like readState.
|
|
311
|
+
* Present when the store surfaces it and on ROOM_SET_ATTENTION acks
|
|
312
|
+
* (which reply READ_STATE scoped to the one room, readState empty). */
|
|
313
|
+
attention?: Record<string, RoomTopicAttention> | undefined;
|
|
299
314
|
requestId?: string | undefined;
|
|
300
315
|
}
|
|
301
316
|
export interface RoomSearchResultsPayload {
|
|
@@ -743,6 +758,15 @@ export interface RoomTopicSetAttentionPayload {
|
|
|
743
758
|
topicId: string;
|
|
744
759
|
attention: RoomTopicAttention;
|
|
745
760
|
}
|
|
761
|
+
/** Room-level attention (notification posture). Private per-user state on
|
|
762
|
+
* the caller's read row; topic attention wins for topic-scoped messages
|
|
763
|
+
* and room attention wins over space attention. The relay acks with a
|
|
764
|
+
* READ_STATE frame scoped to the room (see RoomReadStatePayload). */
|
|
765
|
+
export interface RoomSetAttentionPayload {
|
|
766
|
+
requestId?: string;
|
|
767
|
+
room: string;
|
|
768
|
+
attention: RoomTopicAttention;
|
|
769
|
+
}
|
|
746
770
|
export interface RoomTopicListPayload {
|
|
747
771
|
requestId?: string;
|
|
748
772
|
room: string;
|
|
@@ -1017,6 +1041,7 @@ export type RoomClientMessage = {
|
|
|
1017
1041
|
payload: {
|
|
1018
1042
|
status: RoomUserStatus;
|
|
1019
1043
|
statusText?: string | null;
|
|
1044
|
+
platform?: RoomClientPlatform;
|
|
1020
1045
|
};
|
|
1021
1046
|
timestamp?: number;
|
|
1022
1047
|
} | {
|
|
@@ -1092,6 +1117,10 @@ export type RoomClientMessage = {
|
|
|
1092
1117
|
type: typeof RoomClientMessageType.TOPIC_LIST;
|
|
1093
1118
|
payload: RoomTopicListPayload;
|
|
1094
1119
|
timestamp?: number;
|
|
1120
|
+
} | {
|
|
1121
|
+
type: typeof RoomClientMessageType.ROOM_SET_ATTENTION;
|
|
1122
|
+
payload: RoomSetAttentionPayload;
|
|
1123
|
+
timestamp?: number;
|
|
1095
1124
|
};
|
|
1096
1125
|
export type RoomServerMessage = {
|
|
1097
1126
|
type: typeof RoomServerMessageType.AUTH_SUCCESS;
|
|
@@ -48,6 +48,7 @@ export const RoomClientMessageType = {
|
|
|
48
48
|
TOPIC_RESOLVE: "TOPIC_RESOLVE",
|
|
49
49
|
TOPIC_SET_ATTENTION: "TOPIC_SET_ATTENTION",
|
|
50
50
|
TOPIC_LIST: "TOPIC_LIST",
|
|
51
|
+
ROOM_SET_ATTENTION: "ROOM_SET_ATTENTION",
|
|
51
52
|
};
|
|
52
53
|
export const RoomServerMessageType = {
|
|
53
54
|
AUTH_SUCCESS: "AUTH_SUCCESS",
|
package/dist/social-types.d.ts
CHANGED
|
@@ -130,6 +130,9 @@ export interface RoomDmMessage {
|
|
|
130
130
|
emoji: string;
|
|
131
131
|
users: string[];
|
|
132
132
|
}>;
|
|
133
|
+
/** Client-minted idempotency tag echoed back on DM sends, so a sender's
|
|
134
|
+
* durable queue can settle its optimistic row. Additive (0.4.0). */
|
|
135
|
+
clientMsgId?: string;
|
|
133
136
|
}
|
|
134
137
|
export interface RoomDmHistory {
|
|
135
138
|
conversationId: string;
|
package/dist/types.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export type { RoomAccessMode, RoomAttachment, RoomAttachmentInput, RoomAuditEntry, RoomAuditLogPayload, RoomAuditLogRequestPayload, RoomAuthSuccessPayload, RoomBackfillPayload, RoomClientMessage, RoomCreatedPayload, RoomCreatePayload, RoomDmHistoryPayload, RoomDmHistoryResultPayload, RoomDmInboxUpdatedPayload, RoomDmMessage, RoomDmMessagePayload, RoomDmOpenedPayload, RoomDmOpenPayload, RoomDmParticipant, RoomDmReadPayload, RoomDmSendPayload, RoomEntryUpdatedPayload, RoomErrorPayload, RoomFriendPresence, RoomInvitedPayload, RoomInvitePayload, RoomLedgerEntry, RoomMember, RoomMemberRemovedPayload, RoomMemberRoleChangedPayload, RoomMemberTier, RoomMessagePayload, RoomMeta, RoomModerateMemberPayload, RoomMutedPayload, RoomPinsPayload, RoomPresenceDeltaPayload, RoomPresencePayload, RoomReactorsPayload, RoomReadStatePayload, RoomRemoveMemberPayload, RoomReportedPayload, RoomReportPayload, RoomResyncPayload, RoomRole, RoomRoleChangePayload, RoomSearchPayload, RoomSearchResultsPayload, RoomSearchScope, RoomServerMessage, RoomServerMessageOf, RoomSetReadOnlyPayload, RoomSpace, RoomSpaceJoinedPayload, RoomSpacesPayload, RoomThreadPayload, RoomTopic, RoomTopicAttention, RoomTopicBucketState, RoomTopicCreatePayload, RoomTopicListPayload, RoomTopicMergePayload, RoomTopicMovePayload, RoomTopicRenamePayload, RoomTopicResolvePayload, RoomTopicResultPayload, RoomTopicSetAttentionPayload, RoomTopicsPolicy, RoomTopicsResultPayload, RoomTopicUpdatedPayload, RoomTypingPayload, RoomUserStatus, RoomWhoPayload, } from "./shared/rooms-protocol.js";
|
|
1
|
+
export type { RoomAccessMode, RoomAttachment, RoomAttachmentInput, RoomAuditEntry, RoomAuditLogPayload, RoomAuditLogRequestPayload, RoomAuthSuccessPayload, RoomBackfillPayload, RoomClientMessage, RoomClientPlatform, RoomCreatedPayload, RoomCreatePayload, RoomDmHistoryPayload, RoomDmHistoryResultPayload, RoomDmInboxUpdatedPayload, RoomDmMessage, RoomDmMessagePayload, RoomDmOpenedPayload, RoomDmOpenPayload, RoomDmParticipant, RoomDmReadPayload, RoomDmSendPayload, RoomEntryUpdatedPayload, RoomErrorPayload, RoomFriendPresence, RoomInvitedPayload, RoomInvitePayload, RoomLedgerEntry, RoomMember, RoomMemberRemovedPayload, RoomMemberRoleChangedPayload, RoomMemberTier, RoomMessagePayload, RoomMeta, RoomModerateMemberPayload, RoomMutedPayload, RoomPinsPayload, RoomPresenceDeltaPayload, RoomPresencePayload, RoomReactorsPayload, RoomReadStatePayload, RoomRemoveMemberPayload, RoomReportedPayload, RoomReportPayload, RoomResyncPayload, RoomRole, RoomRoleChangePayload, RoomSearchPayload, RoomSearchResultsPayload, RoomSearchScope, RoomServerMessage, RoomServerMessageOf, RoomSetAttentionPayload, RoomSetReadOnlyPayload, RoomSpace, RoomSpaceJoinedPayload, RoomSpacesPayload, RoomThreadPayload, RoomTopic, RoomTopicAttention, RoomTopicBucketState, RoomTopicCreatePayload, RoomTopicListPayload, RoomTopicMergePayload, RoomTopicMovePayload, RoomTopicRenamePayload, RoomTopicResolvePayload, RoomTopicResultPayload, RoomTopicSetAttentionPayload, RoomTopicsPolicy, RoomTopicsResultPayload, RoomTopicUpdatedPayload, RoomTypingPayload, RoomUserStatus, RoomWhoPayload, } from "./shared/rooms-protocol.js";
|
|
2
2
|
export { applyRoomPresenceDelta, isSystemRoomLedgerEntryType, ROOM_PRESENCE_SAMPLE_LIMIT, RoomClientMessageType, RoomLedgerEntryType, RoomServerMessageType, SUPPORTED_ROOM_PROTOCOL_VERSION, } from "./shared/rooms-protocol.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openmarket/rooms-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "OM Rooms protocol client: wire types, WebSocket + REST clients, and chat view-models. Browser-safe (no node:/bun: imports).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
package/src/client.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
type RoomBackfillPayload,
|
|
14
14
|
type RoomClientMessage,
|
|
15
15
|
RoomClientMessageType,
|
|
16
|
+
type RoomClientPlatform,
|
|
16
17
|
type RoomDmHistoryResultPayload,
|
|
17
18
|
type RoomDmMessagePayload,
|
|
18
19
|
type RoomDmOpenedPayload,
|
|
@@ -1147,6 +1148,15 @@ export function attachRejoinOnReconnect(
|
|
|
1147
1148
|
});
|
|
1148
1149
|
}
|
|
1149
1150
|
|
|
1151
|
+
/**
|
|
1152
|
+
* Post into a joined room and return the frame's clientMsgId (what MESSAGE
|
|
1153
|
+
* echoes carry back for optimistic-send reconciliation). `clientMsgId` is
|
|
1154
|
+
* caller-suppliable so an offline send queue can retry one logical message
|
|
1155
|
+
* under one stable id (same contract as sayRoom / sendRoomDmOnClient);
|
|
1156
|
+
* omitted, it mints a UUID exactly as before. The relay validates the id
|
|
1157
|
+
* (1..128 chars after trim); dedupe on (userId, clientMsgId) is server-side
|
|
1158
|
+
* work and NOT implied here.
|
|
1159
|
+
*/
|
|
1150
1160
|
export function postRoomMessage(
|
|
1151
1161
|
client: RoomWsClient,
|
|
1152
1162
|
room: string,
|
|
@@ -1154,21 +1164,22 @@ export function postRoomMessage(
|
|
|
1154
1164
|
inReplyTo?: number,
|
|
1155
1165
|
attachments?: RoomAttachmentInput[],
|
|
1156
1166
|
topicId?: string,
|
|
1167
|
+
clientMsgId?: string,
|
|
1157
1168
|
): string {
|
|
1158
|
-
const
|
|
1169
|
+
const msgId = clientMsgId ?? randomUUID();
|
|
1159
1170
|
client.send({
|
|
1160
1171
|
type: RoomClientMessageType.POST,
|
|
1161
1172
|
payload: {
|
|
1162
1173
|
room,
|
|
1163
1174
|
...(text ? { text } : {}),
|
|
1164
1175
|
...(attachments?.length ? { attachments: toRoomAttachmentPayloads(attachments) } : {}),
|
|
1165
|
-
clientMsgId,
|
|
1176
|
+
clientMsgId: msgId,
|
|
1166
1177
|
...(inReplyTo !== undefined ? { inReplyTo } : {}),
|
|
1167
1178
|
...(topicId ? { topicId } : {}),
|
|
1168
1179
|
},
|
|
1169
1180
|
timestamp: Date.now(),
|
|
1170
1181
|
});
|
|
1171
|
-
return
|
|
1182
|
+
return msgId;
|
|
1172
1183
|
}
|
|
1173
1184
|
|
|
1174
1185
|
export function sendRoomTyping(client: RoomWsClient, room: string, state: "start" | "stop"): void {
|
|
@@ -1184,10 +1195,17 @@ export function sendRoomStatus(
|
|
|
1184
1195
|
client: RoomWsClient,
|
|
1185
1196
|
status: RoomUserStatus,
|
|
1186
1197
|
statusText?: string | null,
|
|
1198
|
+
platform?: RoomClientPlatform,
|
|
1187
1199
|
): void {
|
|
1188
1200
|
client.send({
|
|
1189
1201
|
type: RoomClientMessageType.STATUS,
|
|
1190
|
-
payload: {
|
|
1202
|
+
payload: {
|
|
1203
|
+
status,
|
|
1204
|
+
...(statusText !== undefined ? { statusText } : {}),
|
|
1205
|
+
// Session identity, declared once per connection (or on change): the
|
|
1206
|
+
// relay carries it onto presence member entries.
|
|
1207
|
+
...(platform ? { platform } : {}),
|
|
1208
|
+
},
|
|
1191
1209
|
timestamp: Date.now(),
|
|
1192
1210
|
});
|
|
1193
1211
|
}
|
|
@@ -1393,11 +1411,12 @@ export async function setRoomStatusOnce(
|
|
|
1393
1411
|
config: RoomClientConfig,
|
|
1394
1412
|
status: RoomUserStatus,
|
|
1395
1413
|
statusText?: string | null,
|
|
1414
|
+
platform?: RoomClientPlatform,
|
|
1396
1415
|
): Promise<void> {
|
|
1397
1416
|
const client = new RoomWsClient(config);
|
|
1398
1417
|
try {
|
|
1399
1418
|
await client.connect();
|
|
1400
|
-
sendRoomStatus(client, status, statusText);
|
|
1419
|
+
sendRoomStatus(client, status, statusText, platform);
|
|
1401
1420
|
// STATUS has no ack frame: give the socket a beat to flush before close.
|
|
1402
1421
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
1403
1422
|
} finally {
|
|
@@ -1405,6 +1424,33 @@ export async function setRoomStatusOnce(
|
|
|
1405
1424
|
}
|
|
1406
1425
|
}
|
|
1407
1426
|
|
|
1427
|
+
/**
|
|
1428
|
+
* Set the caller's room-level attention (notification posture) on a live
|
|
1429
|
+
* client. The relay validates room participation, persists through the
|
|
1430
|
+
* store, and acks with a READ_STATE frame scoped to this room whose
|
|
1431
|
+
* attention map carries the persisted value (readState stays empty: cursors
|
|
1432
|
+
* are untouched). Private per-user state; nothing broadcasts.
|
|
1433
|
+
*/
|
|
1434
|
+
export async function setRoomAttentionOnClient(
|
|
1435
|
+
client: RoomWsClient,
|
|
1436
|
+
request: { room: string; attention: RoomTopicAttention },
|
|
1437
|
+
): Promise<RoomReadStatePayload> {
|
|
1438
|
+
const requestId = randomUUID();
|
|
1439
|
+
const room = normalizeRoomName(request.room);
|
|
1440
|
+
client.send({
|
|
1441
|
+
type: RoomClientMessageType.ROOM_SET_ATTENTION,
|
|
1442
|
+
payload: { requestId, room, attention: request.attention },
|
|
1443
|
+
timestamp: Date.now(),
|
|
1444
|
+
});
|
|
1445
|
+
const result = await client.waitFor(
|
|
1446
|
+
RoomServerMessageType.READ_STATE,
|
|
1447
|
+
(msg) => msg.payload.requestId === requestId,
|
|
1448
|
+
undefined,
|
|
1449
|
+
requestId,
|
|
1450
|
+
);
|
|
1451
|
+
return result.payload;
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1408
1454
|
/** One-shot session helper for the config-based mutation verbs below (the
|
|
1409
1455
|
* agent tools' transport: connect, join, act, wait for the echo, close). */
|
|
1410
1456
|
async function withRoomEcho(
|
|
@@ -48,6 +48,7 @@ export const RoomClientMessageType = {
|
|
|
48
48
|
TOPIC_RESOLVE: "TOPIC_RESOLVE",
|
|
49
49
|
TOPIC_SET_ATTENTION: "TOPIC_SET_ATTENTION",
|
|
50
50
|
TOPIC_LIST: "TOPIC_LIST",
|
|
51
|
+
ROOM_SET_ATTENTION: "ROOM_SET_ATTENTION",
|
|
51
52
|
} as const;
|
|
52
53
|
|
|
53
54
|
export type RoomClientMessageType =
|
|
@@ -152,6 +153,9 @@ export type RoomRole = "owner" | "admin" | "mod" | "member";
|
|
|
152
153
|
* semantics of an absent value). */
|
|
153
154
|
export type RoomTopicsPolicy = "off" | "allowed" | "required";
|
|
154
155
|
export type RoomTopicAttention = "follow" | "default" | "mute";
|
|
156
|
+
/** Self-declared client class on STATUS frames; presence member entries
|
|
157
|
+
* carry it through so rosters can render where someone is connected from. */
|
|
158
|
+
export type RoomClientPlatform = "web" | "desktop" | "mobile" | "tui";
|
|
155
159
|
|
|
156
160
|
export interface RoomMember {
|
|
157
161
|
userId: string;
|
|
@@ -166,6 +170,8 @@ export interface RoomMember {
|
|
|
166
170
|
joinedAt: number;
|
|
167
171
|
avatarUrl?: string | null;
|
|
168
172
|
statusText?: string | null;
|
|
173
|
+
/** Present when the member's session declared one via STATUS. */
|
|
174
|
+
platform?: RoomClientPlatform | undefined;
|
|
169
175
|
}
|
|
170
176
|
|
|
171
177
|
export interface RoomMeta {
|
|
@@ -191,6 +197,10 @@ export interface RoomMeta {
|
|
|
191
197
|
* 2026-07-12 dial-default flip (post-flip relays always advertise the
|
|
192
198
|
* stored value, "off" included); explicit "off" means topics are inert. */
|
|
193
199
|
topicsPolicy?: RoomTopicsPolicy | undefined;
|
|
200
|
+
/** The viewer's room attention (notification posture), a per-viewer field
|
|
201
|
+
* like viewerRole: surfaced on rooms listings for the caller. Absent
|
|
202
|
+
* reads as "default". */
|
|
203
|
+
attention?: RoomTopicAttention | undefined;
|
|
194
204
|
tags?: string[] | undefined;
|
|
195
205
|
entities?: string[] | undefined;
|
|
196
206
|
sourceRefs?: Array<Record<string, unknown>> | undefined;
|
|
@@ -331,7 +341,12 @@ export interface RoomPinsPayload {
|
|
|
331
341
|
}
|
|
332
342
|
|
|
333
343
|
export interface RoomReadStatePayload {
|
|
344
|
+
/** Partial map: rooms absent from a reply are unchanged. */
|
|
334
345
|
readState: Record<string, number>;
|
|
346
|
+
/** Per-room attention for the requesting user, keyed like readState.
|
|
347
|
+
* Present when the store surfaces it and on ROOM_SET_ATTENTION acks
|
|
348
|
+
* (which reply READ_STATE scoped to the one room, readState empty). */
|
|
349
|
+
attention?: Record<string, RoomTopicAttention> | undefined;
|
|
335
350
|
requestId?: string | undefined;
|
|
336
351
|
}
|
|
337
352
|
|
|
@@ -862,6 +877,16 @@ export interface RoomTopicSetAttentionPayload {
|
|
|
862
877
|
attention: RoomTopicAttention;
|
|
863
878
|
}
|
|
864
879
|
|
|
880
|
+
/** Room-level attention (notification posture). Private per-user state on
|
|
881
|
+
* the caller's read row; topic attention wins for topic-scoped messages
|
|
882
|
+
* and room attention wins over space attention. The relay acks with a
|
|
883
|
+
* READ_STATE frame scoped to the room (see RoomReadStatePayload). */
|
|
884
|
+
export interface RoomSetAttentionPayload {
|
|
885
|
+
requestId?: string;
|
|
886
|
+
room: string;
|
|
887
|
+
attention: RoomTopicAttention;
|
|
888
|
+
}
|
|
889
|
+
|
|
865
890
|
export interface RoomTopicListPayload {
|
|
866
891
|
requestId?: string;
|
|
867
892
|
room: string;
|
|
@@ -1129,7 +1154,11 @@ export type RoomClientMessage =
|
|
|
1129
1154
|
}
|
|
1130
1155
|
| {
|
|
1131
1156
|
type: typeof RoomClientMessageType.STATUS;
|
|
1132
|
-
payload: {
|
|
1157
|
+
payload: {
|
|
1158
|
+
status: RoomUserStatus;
|
|
1159
|
+
statusText?: string | null;
|
|
1160
|
+
platform?: RoomClientPlatform;
|
|
1161
|
+
};
|
|
1133
1162
|
timestamp?: number;
|
|
1134
1163
|
}
|
|
1135
1164
|
| {
|
|
@@ -1213,6 +1242,11 @@ export type RoomClientMessage =
|
|
|
1213
1242
|
type: typeof RoomClientMessageType.TOPIC_LIST;
|
|
1214
1243
|
payload: RoomTopicListPayload;
|
|
1215
1244
|
timestamp?: number;
|
|
1245
|
+
}
|
|
1246
|
+
| {
|
|
1247
|
+
type: typeof RoomClientMessageType.ROOM_SET_ATTENTION;
|
|
1248
|
+
payload: RoomSetAttentionPayload;
|
|
1249
|
+
timestamp?: number;
|
|
1216
1250
|
};
|
|
1217
1251
|
|
|
1218
1252
|
export type RoomServerMessage =
|
package/src/social-types.ts
CHANGED
|
@@ -144,6 +144,9 @@ export interface RoomDmMessage {
|
|
|
144
144
|
imageUrl?: string | null;
|
|
145
145
|
/** Aggregated reactions (per-emoji userId lists). Additive. */
|
|
146
146
|
reactions?: Array<{ emoji: string; users: string[] }>;
|
|
147
|
+
/** Client-minted idempotency tag echoed back on DM sends, so a sender's
|
|
148
|
+
* durable queue can settle its optimistic row. Additive (0.4.0). */
|
|
149
|
+
clientMsgId?: string;
|
|
147
150
|
}
|
|
148
151
|
|
|
149
152
|
export interface RoomDmHistory {
|
package/src/types.ts
CHANGED
|
@@ -8,6 +8,7 @@ export type {
|
|
|
8
8
|
RoomAuthSuccessPayload,
|
|
9
9
|
RoomBackfillPayload,
|
|
10
10
|
RoomClientMessage,
|
|
11
|
+
RoomClientPlatform,
|
|
11
12
|
RoomCreatedPayload,
|
|
12
13
|
RoomCreatePayload,
|
|
13
14
|
RoomDmHistoryPayload,
|
|
@@ -50,6 +51,7 @@ export type {
|
|
|
50
51
|
RoomSearchScope,
|
|
51
52
|
RoomServerMessage,
|
|
52
53
|
RoomServerMessageOf,
|
|
54
|
+
RoomSetAttentionPayload,
|
|
53
55
|
RoomSetReadOnlyPayload,
|
|
54
56
|
RoomSpace,
|
|
55
57
|
RoomSpaceJoinedPayload,
|