@openmarket/rooms-client 0.7.1 → 0.9.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/README.md CHANGED
@@ -22,4 +22,23 @@ Bun consumers resolve raw TypeScript via the `bun` export condition; Node and bu
22
22
 
23
23
  ## Versioning
24
24
 
25
- The npm version is managed by hand: bump `package.json` here, `npm publish`, then bump the consumers' pins. The monorepo's release script deliberately skips this package's `package.json`. `src/version.ts` is the exception: it carries the wire/runner version and is auto-bumped with every `om` release so the WebSocket handshake tracks the daemon.
25
+ The npm version is released separately from the OpenMarket application. The
26
+ monorepo release script deliberately skips this package's `package.json`;
27
+ `src/version.ts` is the exception because it carries the wire/runner version.
28
+
29
+ Prepare a version commit with the guarded package gates:
30
+
31
+ ```bash
32
+ bun run rooms-client:release prepare 0.8.0
33
+ ```
34
+
35
+ After that version commit is merged, publish only from a clean, current
36
+ `main` checkout:
37
+
38
+ ```bash
39
+ bun run rooms-client:release publish 0.8.0
40
+ ```
41
+
42
+ The publish command verifies npm identity and version availability, reruns
43
+ the package gates, publishes, and prints the update commands for
44
+ `openmarket-chat` and `openmarket-chat-cloud`. It never commits or pushes.
@@ -24,7 +24,9 @@ export declare class LibraryModel {
24
24
  refresh(): Promise<boolean>;
25
25
  private refreshSpace;
26
26
  private applyChanges;
27
- /** Upsert after our own write so the UI reflects it without waiting a poll. */
27
+ /** Upsert after our own write so the UI reflects it without waiting a poll.
28
+ * Every local write (archive included) advances the receipt: our write IS
29
+ * the head we would otherwise refetch. */
28
30
  applyLocalWrite(doc: RoomDoc): void;
29
31
  docsFor(spaceId: string): RoomDoc[];
30
32
  recentFor(spaceId: string, limit?: number): RoomDoc[];
@@ -34,6 +36,17 @@ export declare class LibraryModel {
34
36
  freshCount(spaceId: string): number;
35
37
  isFresh(spaceId: string, docId: string): boolean;
36
38
  markSeen(spaceId: string): void;
39
+ /**
40
+ * Record a server poke (DOC_CHANGED) that the space's change head moved.
41
+ * Bookkeeping only: latestSeq ratchets up (never down) so atHead() reports
42
+ * stale until a refresh catches up. Returns whether a refresh would fetch
43
+ * anything: false for untracked spaces, true for a seqless poke (an
44
+ * unconditional "something changed"), otherwise whether the poked head is
45
+ * ahead of the cursor. Callers own debounce/refresh scheduling, and must
46
+ * re-check atHead() after a refresh completes: the refresh() single-flight
47
+ * guard drops a poke that lands mid-refresh.
48
+ */
49
+ noteRemoteHead(spaceId: string, latestSeq?: number): boolean;
37
50
  /** True when the cache has provably caught up to the space's change head. */
38
51
  atHead(spaceId: string): boolean;
39
52
  syncedOnce(spaceId: string): boolean;
@@ -95,21 +95,23 @@ export class LibraryModel {
95
95
  }
96
96
  return changed;
97
97
  }
98
- /** Upsert after our own write so the UI reflects it without waiting a poll. */
98
+ /** Upsert after our own write so the UI reflects it without waiting a poll.
99
+ * Every local write (archive included) advances the receipt: our write IS
100
+ * the head we would otherwise refetch. */
99
101
  applyLocalWrite(doc) {
100
102
  const state = this.spaces.get(doc.spaceId);
101
103
  if (!state)
102
104
  return;
105
+ if (doc.spaceDocSeq > state.cursor) {
106
+ state.cursor = doc.spaceDocSeq;
107
+ state.latestSeq = Math.max(state.latestSeq, doc.spaceDocSeq);
108
+ }
103
109
  if (doc.status === "archived") {
104
110
  state.docs.delete(doc.docId);
105
111
  state.freshDocIds.delete(doc.docId);
106
112
  return;
107
113
  }
108
114
  state.docs.set(doc.docId, doc);
109
- if (doc.spaceDocSeq > state.cursor) {
110
- state.cursor = doc.spaceDocSeq;
111
- state.latestSeq = Math.max(state.latestSeq, doc.spaceDocSeq);
112
- }
113
115
  }
114
116
  docsFor(spaceId) {
115
117
  const state = this.spaces.get(spaceId);
@@ -157,6 +159,25 @@ export class LibraryModel {
157
159
  markSeen(spaceId) {
158
160
  this.spaces.get(spaceId)?.freshDocIds.clear();
159
161
  }
162
+ /**
163
+ * Record a server poke (DOC_CHANGED) that the space's change head moved.
164
+ * Bookkeeping only: latestSeq ratchets up (never down) so atHead() reports
165
+ * stale until a refresh catches up. Returns whether a refresh would fetch
166
+ * anything: false for untracked spaces, true for a seqless poke (an
167
+ * unconditional "something changed"), otherwise whether the poked head is
168
+ * ahead of the cursor. Callers own debounce/refresh scheduling, and must
169
+ * re-check atHead() after a refresh completes: the refresh() single-flight
170
+ * guard drops a poke that lands mid-refresh.
171
+ */
172
+ noteRemoteHead(spaceId, latestSeq) {
173
+ const state = this.spaces.get(spaceId);
174
+ if (!state)
175
+ return false;
176
+ if (latestSeq === undefined)
177
+ return true;
178
+ state.latestSeq = Math.max(state.latestSeq, latestSeq);
179
+ return latestSeq > state.cursor;
180
+ }
160
181
  /** True when the cache has provably caught up to the space's change head. */
161
182
  atHead(spaceId) {
162
183
  const state = this.spaces.get(spaceId);
package/dist/client.d.ts CHANGED
@@ -212,6 +212,7 @@ export interface RoomDmSendRequest {
212
212
  * URL. Stored on the message's imageUrl; the client signs keys on render.
213
213
  * Additive — omitted for text-only sends. */
214
214
  imageUrl?: string;
215
+ attachments?: RoomAttachmentInput[];
215
216
  metadata?: RoomForwardMetadata;
216
217
  }
217
218
  export interface RoomDmReplyRequest extends RoomDmSendRequest {
package/dist/client.js CHANGED
@@ -489,6 +489,9 @@ export async function sendRoomDmOnClient(client, request, timeoutMs) {
489
489
  ...(request.username ? { username: request.username } : {}),
490
490
  ...(request.conversationId ? { conversationId: request.conversationId } : {}),
491
491
  ...(request.imageUrl ? { imageUrl: request.imageUrl } : {}),
492
+ ...(request.attachments?.length
493
+ ? { attachments: toRoomAttachmentPayloads(request.attachments) }
494
+ : {}),
492
495
  ...(request.metadata ? { metadata: request.metadata } : {}),
493
496
  },
494
497
  timestamp: Date.now(),
@@ -511,6 +514,9 @@ export async function replyRoomDmOnClient(client, request, timeoutMs) {
511
514
  ...(request.username ? { username: request.username } : {}),
512
515
  ...(request.conversationId ? { conversationId: request.conversationId } : {}),
513
516
  ...(request.imageUrl ? { imageUrl: request.imageUrl } : {}),
517
+ ...(request.attachments?.length
518
+ ? { attachments: toRoomAttachmentPayloads(request.attachments) }
519
+ : {}),
514
520
  ...(request.metadata ? { metadata: request.metadata } : {}),
515
521
  },
516
522
  timestamp: Date.now(),
@@ -98,11 +98,14 @@ export declare const RoomServerMessageType: {
98
98
  readonly DM_MESSAGE_EDITED: "DM_MESSAGE_EDITED";
99
99
  readonly DM_MESSAGE_DELETED: "DM_MESSAGE_DELETED";
100
100
  readonly DM_REACTION_UPDATED: "DM_REACTION_UPDATED";
101
+ readonly DM_ATTACHMENT_UPDATED: "DM_ATTACHMENT_UPDATED";
101
102
  readonly DM_PIN_UPDATED: "DM_PIN_UPDATED";
102
103
  readonly DM_PINS: "DM_PINS";
103
104
  readonly TOPIC: "TOPIC";
104
105
  readonly TOPICS: "TOPICS";
105
106
  readonly TOPIC_UPDATED: "TOPIC_UPDATED";
107
+ readonly DOC_CHANGED: "DOC_CHANGED";
108
+ readonly FRIEND_UPDATED: "FRIEND_UPDATED";
106
109
  };
107
110
  export declare function addBounded<T>(set: Set<T>, value: T, max: number): void;
108
111
  export type RoomServerMessageType = (typeof RoomServerMessageType)[keyof typeof RoomServerMessageType];
@@ -278,10 +281,11 @@ export interface RoomWebhookMetadata {
278
281
  }
279
282
  export interface RoomAttachment {
280
283
  /**
281
- * Object key under room-attachments/; clients sign it at render time.
282
- * Absent while scanStatus is "pending" on entries served to members: the
283
- * store withholds the signable key until the scan clears, so render the
284
- * metadata as a placeholder and do not attempt signing without a key.
284
+ * Object key under room-attachments/ or dm-attachments/; clients sign it
285
+ * at render time. Absent while scanStatus is "pending" on entries served
286
+ * to recipients: the store withholds the signable key until the scan
287
+ * clears, so render the metadata as a placeholder and do not attempt
288
+ * signing without a key.
285
289
  */
286
290
  key?: string | undefined;
287
291
  name: string;
@@ -292,7 +296,7 @@ export interface RoomAttachment {
292
296
  /**
293
297
  * A postable attachment reference. Outbound frames must always carry the
294
298
  * stored key (senders reference their own uploads, which include it); only
295
- * read views of pending entries may be keyless, and those must never be
299
+ * recipient views of pending entries may be keyless, and those must never be
296
300
  * forwarded back into a post.
297
301
  */
298
302
  export type RoomAttachmentInput = RoomAttachment & {
@@ -619,6 +623,7 @@ export interface RoomDmSendPayload {
619
623
  /** DM-attachment key or legacy public image URL; stored on the message's
620
624
  * imageUrl. Additive — text-only sends omit it. */
621
625
  imageUrl?: string;
626
+ attachments?: RoomAttachmentInput[];
622
627
  metadata?: RoomForwardMetadata | undefined;
623
628
  }
624
629
  /** Client → server: send a DM whose parent is another message in the same conversation. */
@@ -677,6 +682,12 @@ export interface RoomDmReactionUpdatedPayload {
677
682
  }>;
678
683
  participant: RoomDmParticipant;
679
684
  }
685
+ export interface RoomDmAttachmentUpdatedPayload {
686
+ conversationId: string;
687
+ messageId: string;
688
+ scanStatus: "clean" | "held";
689
+ attachments: RoomAttachment[];
690
+ }
680
691
  export interface RoomDmPinPayload {
681
692
  requestId?: string;
682
693
  conversationId?: string;
@@ -966,6 +977,19 @@ export interface RoomTopicUpdatedPayload {
966
977
  room: string;
967
978
  topic: RoomTopic;
968
979
  }
980
+ /** Poke: a space's doc-library change head moved (create/edit/archive/restore).
981
+ * Deliberately content-free: clients refetch deltas over REST. */
982
+ export interface RoomDocChangedPayload {
983
+ spaceId: string;
984
+ /** The space's change head after the mutation, when the server knows it
985
+ * cheaply; absent still means "something changed, refetch". */
986
+ latestSeq?: number | undefined;
987
+ }
988
+ /** Poke: the recipient's friendship graph changed. Clients refetch over REST. */
989
+ export interface RoomFriendUpdatedPayload {
990
+ kind: "request" | "accepted" | "rejected" | "removed";
991
+ friendshipId?: string | undefined;
992
+ }
969
993
  export type RoomClientMessage = {
970
994
  type: typeof RoomClientMessageType.AUTHENTICATE;
971
995
  payload: {
@@ -1471,6 +1495,10 @@ export type RoomServerMessage = {
1471
1495
  type: typeof RoomServerMessageType.DM_REACTION_UPDATED;
1472
1496
  payload: RoomDmReactionUpdatedPayload;
1473
1497
  timestamp: number;
1498
+ } | {
1499
+ type: typeof RoomServerMessageType.DM_ATTACHMENT_UPDATED;
1500
+ payload: RoomDmAttachmentUpdatedPayload;
1501
+ timestamp: number;
1474
1502
  } | {
1475
1503
  type: typeof RoomServerMessageType.DM_PIN_UPDATED;
1476
1504
  payload: RoomDmPinUpdatedPayload;
@@ -1491,6 +1519,14 @@ export type RoomServerMessage = {
1491
1519
  type: typeof RoomServerMessageType.TOPIC_UPDATED;
1492
1520
  payload: RoomTopicUpdatedPayload;
1493
1521
  timestamp: number;
1522
+ } | {
1523
+ type: typeof RoomServerMessageType.DOC_CHANGED;
1524
+ payload: RoomDocChangedPayload;
1525
+ timestamp: number;
1526
+ } | {
1527
+ type: typeof RoomServerMessageType.FRIEND_UPDATED;
1528
+ payload: RoomFriendUpdatedPayload;
1529
+ timestamp: number;
1494
1530
  };
1495
1531
  export type RoomServerMessageOf<T extends RoomServerMessageType> = Extract<RoomServerMessage, {
1496
1532
  type: T;
@@ -97,11 +97,14 @@ export const RoomServerMessageType = {
97
97
  DM_MESSAGE_EDITED: "DM_MESSAGE_EDITED",
98
98
  DM_MESSAGE_DELETED: "DM_MESSAGE_DELETED",
99
99
  DM_REACTION_UPDATED: "DM_REACTION_UPDATED",
100
+ DM_ATTACHMENT_UPDATED: "DM_ATTACHMENT_UPDATED",
100
101
  DM_PIN_UPDATED: "DM_PIN_UPDATED",
101
102
  DM_PINS: "DM_PINS",
102
103
  TOPIC: "TOPIC",
103
104
  TOPICS: "TOPICS",
104
105
  TOPIC_UPDATED: "TOPIC_UPDATED",
106
+ DOC_CHANGED: "DOC_CHANGED",
107
+ FRIEND_UPDATED: "FRIEND_UPDATED",
105
108
  };
106
109
  export function addBounded(set, value, max) {
107
110
  set.add(value);
@@ -604,8 +604,10 @@ export declare function socialRequest<T>(config: RoomSocialConfig, path: string,
604
604
  formData?: FormData;
605
605
  headers?: Record<string, string>;
606
606
  /** Fetch cache mode; token-bearing reads pass "no-store" so posting
607
- * credentials never persist in a browser HTTP cache. */
608
- cache?: RequestCache;
607
+ * credentials never persist in a browser HTTP cache. Spelled as the
608
+ * literal union rather than DOM's RequestCache: this package
609
+ * typechecks without the DOM lib. */
610
+ cache?: "default" | "force-cache" | "no-cache" | "no-store" | "only-if-cached" | "reload";
609
611
  }): Promise<T>;
610
612
  export declare function cleanUsername(username: string): string;
611
613
  export declare function cleanRoomId(room: string): string;
@@ -125,6 +125,8 @@ export interface RoomDmMessage {
125
125
  /** DM image: a private dm-attachments/ key (signed on render) or a legacy
126
126
  * public URL. Additive — old messages carry none. */
127
127
  imageUrl?: string | null;
128
+ /** Private scanned attachments. Keys are absent until the bytes are clean. */
129
+ attachments?: RoomAttachment[];
128
130
  /** Aggregated reactions (per-emoji userId lists). Additive. */
129
131
  reactions?: Array<{
130
132
  emoji: string;
package/dist/types.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export type { RoomAccessMode, RoomAttachment, RoomAttachmentInput, RoomAuditEntry, RoomAuditLogPayload, RoomAuditLogRequestPayload, RoomAuthSuccessPayload, RoomBackfillPayload, RoomClientMessage, RoomClientPlatform, RoomCreatedPayload, RoomCreatePayload, RoomDmHistoryPayload, RoomDmHistoryResultPayload, RoomDmInboxUpdatedPayload, RoomDmMessage, RoomDmMessageDeletedPayload, RoomDmMessageEditedPayload, RoomDmMessagePayload, RoomDmOpenedPayload, RoomDmOpenPayload, RoomDmParticipant, RoomDmPinsResultPayload, RoomDmPinUpdatedPayload, RoomDmReactionUpdatedPayload, RoomDmReadPayload, RoomDmReplyPayload, RoomDmSendPayload, RoomEntryUpdatedPayload, RoomErrorPayload, RoomForwardedFrom, RoomForwardMetadata, RoomFriendPresence, RoomInvitedPayload, RoomInvitePayload, RoomLedgerEntry, RoomMember, RoomMemberRemovedPayload, RoomMemberRoleChangedPayload, RoomMemberTier, RoomMessagePayload, RoomMeta, RoomModerateMemberPayload, RoomMutedPayload, RoomPinsPayload, RoomPollOption, RoomPollSummary, 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, RoomUnfurl, RoomUserStatus, RoomWhoPayload, } from "./shared/rooms-protocol.js";
1
+ export type { RoomAccessMode, RoomAttachment, RoomAttachmentInput, RoomAuditEntry, RoomAuditLogPayload, RoomAuditLogRequestPayload, RoomAuthSuccessPayload, RoomBackfillPayload, RoomClientMessage, RoomClientPlatform, RoomCreatedPayload, RoomCreatePayload, RoomDmAttachmentUpdatedPayload, RoomDmHistoryPayload, RoomDmHistoryResultPayload, RoomDmInboxUpdatedPayload, RoomDmMessage, RoomDmMessageDeletedPayload, RoomDmMessageEditedPayload, RoomDmMessagePayload, RoomDmOpenedPayload, RoomDmOpenPayload, RoomDmParticipant, RoomDmPinsResultPayload, RoomDmPinUpdatedPayload, RoomDmReactionUpdatedPayload, RoomDmReadPayload, RoomDmReplyPayload, RoomDmSendPayload, RoomEntryUpdatedPayload, RoomErrorPayload, RoomForwardedFrom, RoomForwardMetadata, RoomFriendPresence, RoomInvitedPayload, RoomInvitePayload, RoomLedgerEntry, RoomMember, RoomMemberRemovedPayload, RoomMemberRoleChangedPayload, RoomMemberTier, RoomMessagePayload, RoomMeta, RoomModerateMemberPayload, RoomMutedPayload, RoomPinsPayload, RoomPollOption, RoomPollSummary, 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, RoomUnfurl, 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/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "0.7.0";
2
- export declare const RUNNER_VERSION = "0.7.0";
1
+ export declare const VERSION = "0.9.0";
2
+ export declare const RUNNER_VERSION = "0.9.0";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
- export const VERSION = "0.7.0";
2
- export const RUNNER_VERSION = "0.7.0";
1
+ export const VERSION = "0.9.0";
2
+ export const RUNNER_VERSION = "0.9.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openmarket/rooms-client",
3
- "version": "0.7.1",
3
+ "version": "0.9.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",
@@ -170,9 +170,7 @@ export function ownSeqsOf(entries: ReadonlyArray<RoomLedgerEntry>, ownUserId: st
170
170
  * autonomous turn, and webhook chatter cannot satisfy the
171
171
  * human-activity brake. */
172
172
  function isWebhookEntry(entry: RoomLedgerEntry): boolean {
173
- return Boolean(
174
- (entry.metadata as { webhook?: unknown } | undefined)?.webhook,
175
- );
173
+ return Boolean((entry.metadata as { webhook?: unknown } | undefined)?.webhook);
176
174
  }
177
175
 
178
176
  export function isTriggeringEntry(
@@ -129,20 +129,22 @@ export class LibraryModel {
129
129
  return changed;
130
130
  }
131
131
 
132
- /** Upsert after our own write so the UI reflects it without waiting a poll. */
132
+ /** Upsert after our own write so the UI reflects it without waiting a poll.
133
+ * Every local write (archive included) advances the receipt: our write IS
134
+ * the head we would otherwise refetch. */
133
135
  applyLocalWrite(doc: RoomDoc): void {
134
136
  const state = this.spaces.get(doc.spaceId);
135
137
  if (!state) return;
138
+ if (doc.spaceDocSeq > state.cursor) {
139
+ state.cursor = doc.spaceDocSeq;
140
+ state.latestSeq = Math.max(state.latestSeq, doc.spaceDocSeq);
141
+ }
136
142
  if (doc.status === "archived") {
137
143
  state.docs.delete(doc.docId);
138
144
  state.freshDocIds.delete(doc.docId);
139
145
  return;
140
146
  }
141
147
  state.docs.set(doc.docId, doc);
142
- if (doc.spaceDocSeq > state.cursor) {
143
- state.cursor = doc.spaceDocSeq;
144
- state.latestSeq = Math.max(state.latestSeq, doc.spaceDocSeq);
145
- }
146
148
  }
147
149
 
148
150
  docsFor(spaceId: string): RoomDoc[] {
@@ -193,6 +195,24 @@ export class LibraryModel {
193
195
  this.spaces.get(spaceId)?.freshDocIds.clear();
194
196
  }
195
197
 
198
+ /**
199
+ * Record a server poke (DOC_CHANGED) that the space's change head moved.
200
+ * Bookkeeping only: latestSeq ratchets up (never down) so atHead() reports
201
+ * stale until a refresh catches up. Returns whether a refresh would fetch
202
+ * anything: false for untracked spaces, true for a seqless poke (an
203
+ * unconditional "something changed"), otherwise whether the poked head is
204
+ * ahead of the cursor. Callers own debounce/refresh scheduling, and must
205
+ * re-check atHead() after a refresh completes: the refresh() single-flight
206
+ * guard drops a poke that lands mid-refresh.
207
+ */
208
+ noteRemoteHead(spaceId: string, latestSeq?: number): boolean {
209
+ const state = this.spaces.get(spaceId);
210
+ if (!state) return false;
211
+ if (latestSeq === undefined) return true;
212
+ state.latestSeq = Math.max(state.latestSeq, latestSeq);
213
+ return latestSeq > state.cursor;
214
+ }
215
+
196
216
  /** True when the cache has provably caught up to the space's change head. */
197
217
  atHead(spaceId: string): boolean {
198
218
  const state = this.spaces.get(spaceId);
package/src/client.ts CHANGED
@@ -297,6 +297,7 @@ export interface RoomDmSendRequest {
297
297
  * URL. Stored on the message's imageUrl; the client signs keys on render.
298
298
  * Additive — omitted for text-only sends. */
299
299
  imageUrl?: string;
300
+ attachments?: RoomAttachmentInput[];
300
301
  metadata?: RoomForwardMetadata;
301
302
  }
302
303
 
@@ -994,6 +995,9 @@ export async function sendRoomDmOnClient(
994
995
  ...(request.username ? { username: request.username } : {}),
995
996
  ...(request.conversationId ? { conversationId: request.conversationId } : {}),
996
997
  ...(request.imageUrl ? { imageUrl: request.imageUrl } : {}),
998
+ ...(request.attachments?.length
999
+ ? { attachments: toRoomAttachmentPayloads(request.attachments) }
1000
+ : {}),
997
1001
  ...(request.metadata ? { metadata: request.metadata } : {}),
998
1002
  },
999
1003
  timestamp: Date.now(),
@@ -1027,6 +1031,9 @@ export async function replyRoomDmOnClient(
1027
1031
  ...(request.username ? { username: request.username } : {}),
1028
1032
  ...(request.conversationId ? { conversationId: request.conversationId } : {}),
1029
1033
  ...(request.imageUrl ? { imageUrl: request.imageUrl } : {}),
1034
+ ...(request.attachments?.length
1035
+ ? { attachments: toRoomAttachmentPayloads(request.attachments) }
1036
+ : {}),
1030
1037
  ...(request.metadata ? { metadata: request.metadata } : {}),
1031
1038
  },
1032
1039
  timestamp: Date.now(),
@@ -101,11 +101,14 @@ export const RoomServerMessageType = {
101
101
  DM_MESSAGE_EDITED: "DM_MESSAGE_EDITED",
102
102
  DM_MESSAGE_DELETED: "DM_MESSAGE_DELETED",
103
103
  DM_REACTION_UPDATED: "DM_REACTION_UPDATED",
104
+ DM_ATTACHMENT_UPDATED: "DM_ATTACHMENT_UPDATED",
104
105
  DM_PIN_UPDATED: "DM_PIN_UPDATED",
105
106
  DM_PINS: "DM_PINS",
106
107
  TOPIC: "TOPIC",
107
108
  TOPICS: "TOPICS",
108
109
  TOPIC_UPDATED: "TOPIC_UPDATED",
110
+ DOC_CHANGED: "DOC_CHANGED",
111
+ FRIEND_UPDATED: "FRIEND_UPDATED",
109
112
  } as const;
110
113
 
111
114
  export function addBounded<T>(set: Set<T>, value: T, max: number): void {
@@ -278,7 +281,9 @@ export interface RoomLedgerEntry {
278
281
  * (and servers older than the unfurl worker) omit it. */
279
282
  unfurls?: RoomUnfurl[] | undefined;
280
283
  roomRole?: RoomRole | null | undefined;
281
- metadata?: (Record<string, unknown> & Partial<RoomForwardMetadata> & Partial<RoomWebhookMetadata>) | undefined;
284
+ metadata?:
285
+ | (Record<string, unknown> & Partial<RoomForwardMetadata> & Partial<RoomWebhookMetadata>)
286
+ | undefined;
282
287
  }
283
288
 
284
289
  export type RoomForwardedFrom =
@@ -312,10 +317,11 @@ export interface RoomWebhookMetadata {
312
317
 
313
318
  export interface RoomAttachment {
314
319
  /**
315
- * Object key under room-attachments/; clients sign it at render time.
316
- * Absent while scanStatus is "pending" on entries served to members: the
317
- * store withholds the signable key until the scan clears, so render the
318
- * metadata as a placeholder and do not attempt signing without a key.
320
+ * Object key under room-attachments/ or dm-attachments/; clients sign it
321
+ * at render time. Absent while scanStatus is "pending" on entries served
322
+ * to recipients: the store withholds the signable key until the scan
323
+ * clears, so render the metadata as a placeholder and do not attempt
324
+ * signing without a key.
319
325
  */
320
326
  key?: string | undefined;
321
327
  name: string;
@@ -327,7 +333,7 @@ export interface RoomAttachment {
327
333
  /**
328
334
  * A postable attachment reference. Outbound frames must always carry the
329
335
  * stored key (senders reference their own uploads, which include it); only
330
- * read views of pending entries may be keyless, and those must never be
336
+ * recipient views of pending entries may be keyless, and those must never be
331
337
  * forwarded back into a post.
332
338
  */
333
339
  export type RoomAttachmentInput = RoomAttachment & { key: string };
@@ -683,7 +689,9 @@ export interface RoomDmMessage {
683
689
  createdAt?: string | number | undefined;
684
690
  /** Sealed DM: content is '' and the opaque envelope rides here. */
685
691
  secret?: RoomLedgerSecretPayload | null | undefined;
686
- metadata?: (Record<string, unknown> & Partial<RoomForwardMetadata> & Partial<RoomWebhookMetadata>) | undefined;
692
+ metadata?:
693
+ | (Record<string, unknown> & Partial<RoomForwardMetadata> & Partial<RoomWebhookMetadata>)
694
+ | undefined;
687
695
  /** Legacy public chat-image URL (renders as-is, no signing). */
688
696
  imageUrl?: string | null | undefined;
689
697
  /** Private, signed, scanned DM attachments (dm-attachments/ keyspace).
@@ -718,6 +726,7 @@ export interface RoomDmSendPayload {
718
726
  /** DM-attachment key or legacy public image URL; stored on the message's
719
727
  * imageUrl. Additive — text-only sends omit it. */
720
728
  imageUrl?: string;
729
+ attachments?: RoomAttachmentInput[];
721
730
  metadata?: RoomForwardMetadata | undefined;
722
731
  }
723
732
 
@@ -781,6 +790,13 @@ export interface RoomDmReactionUpdatedPayload {
781
790
  participant: RoomDmParticipant;
782
791
  }
783
792
 
793
+ export interface RoomDmAttachmentUpdatedPayload {
794
+ conversationId: string;
795
+ messageId: string;
796
+ scanStatus: "clean" | "held";
797
+ attachments: RoomAttachment[];
798
+ }
799
+
784
800
  export interface RoomDmPinPayload {
785
801
  requestId?: string;
786
802
  conversationId?: string;
@@ -1104,6 +1120,21 @@ export interface RoomTopicUpdatedPayload {
1104
1120
  topic: RoomTopic;
1105
1121
  }
1106
1122
 
1123
+ /** Poke: a space's doc-library change head moved (create/edit/archive/restore).
1124
+ * Deliberately content-free: clients refetch deltas over REST. */
1125
+ export interface RoomDocChangedPayload {
1126
+ spaceId: string;
1127
+ /** The space's change head after the mutation, when the server knows it
1128
+ * cheaply; absent still means "something changed, refetch". */
1129
+ latestSeq?: number | undefined;
1130
+ }
1131
+
1132
+ /** Poke: the recipient's friendship graph changed. Clients refetch over REST. */
1133
+ export interface RoomFriendUpdatedPayload {
1134
+ kind: "request" | "accepted" | "rejected" | "removed";
1135
+ friendshipId?: string | undefined;
1136
+ }
1137
+
1107
1138
  export type RoomClientMessage =
1108
1139
  | {
1109
1140
  type: typeof RoomClientMessageType.AUTHENTICATE;
@@ -1653,6 +1684,11 @@ export type RoomServerMessage =
1653
1684
  payload: RoomDmReactionUpdatedPayload;
1654
1685
  timestamp: number;
1655
1686
  }
1687
+ | {
1688
+ type: typeof RoomServerMessageType.DM_ATTACHMENT_UPDATED;
1689
+ payload: RoomDmAttachmentUpdatedPayload;
1690
+ timestamp: number;
1691
+ }
1656
1692
  | {
1657
1693
  type: typeof RoomServerMessageType.DM_PIN_UPDATED;
1658
1694
  payload: RoomDmPinUpdatedPayload;
@@ -1677,6 +1713,16 @@ export type RoomServerMessage =
1677
1713
  type: typeof RoomServerMessageType.TOPIC_UPDATED;
1678
1714
  payload: RoomTopicUpdatedPayload;
1679
1715
  timestamp: number;
1716
+ }
1717
+ | {
1718
+ type: typeof RoomServerMessageType.DOC_CHANGED;
1719
+ payload: RoomDocChangedPayload;
1720
+ timestamp: number;
1721
+ }
1722
+ | {
1723
+ type: typeof RoomServerMessageType.FRIEND_UPDATED;
1724
+ payload: RoomFriendUpdatedPayload;
1725
+ timestamp: number;
1680
1726
  };
1681
1727
 
1682
1728
  export type RoomServerMessageOf<T extends RoomServerMessageType> = Extract<
@@ -1934,8 +1934,10 @@ export async function socialRequest<T>(
1934
1934
  formData?: FormData;
1935
1935
  headers?: Record<string, string>;
1936
1936
  /** Fetch cache mode; token-bearing reads pass "no-store" so posting
1937
- * credentials never persist in a browser HTTP cache. */
1938
- cache?: RequestCache;
1937
+ * credentials never persist in a browser HTTP cache. Spelled as the
1938
+ * literal union rather than DOM's RequestCache: this package
1939
+ * typechecks without the DOM lib. */
1940
+ cache?: "default" | "force-cache" | "no-cache" | "no-store" | "only-if-cached" | "reload";
1939
1941
  } = {},
1940
1942
  ): Promise<T> {
1941
1943
  const url = `${trimTrailingSlash(config.apiUrl)}${path}`;
@@ -147,6 +147,8 @@ export interface RoomDmMessage {
147
147
  /** DM image: a private dm-attachments/ key (signed on render) or a legacy
148
148
  * public URL. Additive — old messages carry none. */
149
149
  imageUrl?: string | null;
150
+ /** Private scanned attachments. Keys are absent until the bytes are clean. */
151
+ attachments?: RoomAttachment[];
150
152
  /** Aggregated reactions (per-emoji userId lists). Additive. */
151
153
  reactions?: Array<{ emoji: string; users: string[] }>;
152
154
  /** Shared pin state. Additive; absent means unpinned. */
package/src/types.ts CHANGED
@@ -11,6 +11,7 @@ export type {
11
11
  RoomClientPlatform,
12
12
  RoomCreatedPayload,
13
13
  RoomCreatePayload,
14
+ RoomDmAttachmentUpdatedPayload,
14
15
  RoomDmHistoryPayload,
15
16
  RoomDmHistoryResultPayload,
16
17
  RoomDmInboxUpdatedPayload,
package/src/version.ts CHANGED
@@ -1,2 +1,2 @@
1
- export const VERSION = "0.7.0";
2
- export const RUNNER_VERSION = "0.7.0";
1
+ export const VERSION = "0.9.0";
2
+ export const RUNNER_VERSION = "0.9.0";