Repository: microsoft/FluidFramework
Pull request #24724: docs(client-presence): missing API comments
Author: login: jason-ha
Labels: name: area: framework
description: Framework is a tag for issues involving the developer framework. Eg Aqueduct
name: public api change
description: Changes to a public API
name: base: main
description: PRs targeted against main branch
Created: 2025-05-28T20:09:41Z | Merged: 2025-05-29T21:27:11Z

adds documentation to beta APIs that are not `@system` nor `attendees` or `states` properties of `Presence`

Follow-up for #24710


Diff:
diff --git a/packages/framework/presence/api-report/presence.alpha.api.md b/packages/framework/presence/api-report/presence.alpha.api.md
index 116ba0484450..2ed0ce115573 100644
--- a/packages/framework/presence/api-report/presence.alpha.api.md
+++ b/packages/framework/presence/api-report/presence.alpha.api.md
@@ -16,7 +16,7 @@ export type AttendeeId = SessionId & {
     readonly AttendeeId: "AttendeeId";
 };
 
-// @beta @sealed (undocumented)
+// @beta @sealed
 export interface AttendeesEvents {
     // @eventProperty
     attendeeConnected: (attendee: Attendee) => void;
@@ -153,15 +153,12 @@ export interface LatestArguments<T extends object | null> {
 
 // @beta @sealed
 export interface LatestClientData<T> extends LatestData<T> {
-    // (undocumented)
     attendee: Attendee;
 }
 
 // @beta @sealed
 export interface LatestData<T> {
-    // (undocumented)
     metadata: LatestMetadata;
-    // (undocumented)
     value: DeepReadonly<JsonDeserialized<T>>;
 }
 
@@ -179,23 +176,18 @@ export interface LatestMapArguments<T, Keys extends string | number = string | n
 // @beta @sealed
 export interface LatestMapClientData<T, Keys extends string | number, SpecificAttendeeId extends AttendeeId = AttendeeId> {
     attendee: Attendee<SpecificAttendeeId>;
-    // (undocumented)
     items: ReadonlyMap<Keys, LatestData<T>>;
 }
 
 // @beta @sealed
 export interface LatestMapItemRemovedClientData<K extends string | number> {
-    // (undocumented)
     attendee: Attendee;
-    // (undocumented)
     key: K;
-    // (undocumented)
     metadata: LatestMetadata;
 }
 
 // @beta @sealed
 export interface LatestMapItemUpdatedClientData<T, K extends string | number> extends LatestClientData<T> {
-    // (undocumented)
     key: K;
 }
 
@@ -210,7 +202,7 @@ export interface LatestMapRaw<T, Keys extends string | number = string | number>
     readonly presence: Presence;
 }
 
-// @beta @sealed (undocumented)
+// @beta @sealed
 export interface LatestMapRawEvents<T, K extends string | number> {
     // @eventProperty
     localItemRemoved: (removedItem: {
@@ -247,7 +239,7 @@ export interface LatestRaw<T> {
     readonly presence: Presence;
 }
 
-// @beta @sealed (undocumented)
+// @beta @sealed
 export interface LatestRawEvents<T> {
     // @eventProperty
     localUpdated: (update: {
@@ -320,7 +312,7 @@ export interface Presence {
     };
 }
 
-// @beta @sealed (undocumented)
+// @beta @sealed
 export interface PresenceEvents {
     workspaceActivated: (workspaceAddress: WorkspaceAddress, type: "States" | "Notifications" | "Unknown") => void;
 }
@@ -342,15 +334,12 @@ export const StateFactory: {
 // @beta @sealed
 export interface StateMap<K extends string | number, V> {
     clear(): void;
-    // (undocumented)
     delete(key: K): boolean;
     forEach(callbackfn: (value: DeepReadonly<JsonDeserialized<V>>, key: K, map: StateMap<K, V>) => void, thisArg?: unknown): void;
     get(key: K): DeepReadonly<JsonDeserialized<V>> | undefined;
-    // (undocumented)
     has(key: K): boolean;
     keys(): IterableIterator<K>;
     set(key: K, value: JsonSerializable<V> & JsonDeserialized<V>): this;
-    // (undocumented)
     readonly size: number;
 }
 
@@ -375,7 +364,6 @@ export type StatesWorkspaceEntry<TKey extends string, TValue extends InternalTyp
 
 // @beta
 export interface StatesWorkspaceSchema {
-    // (undocumented)
     [key: string]: StatesWorkspaceEntry<typeof key, InternalTypes.ValueDirectoryOrState<any>>;
 }
 
diff --git a/packages/framework/presence/api-report/presence.beta.api.md b/packages/framework/presence/api-report/presence.beta.api.md
index c68db527364d..240e944302dd 100644
--- a/packages/framework/presence/api-report/presence.beta.api.md
+++ b/packages/framework/presence/api-report/presence.beta.api.md
@@ -16,7 +16,7 @@ export type AttendeeId = SessionId & {
     readonly AttendeeId: "AttendeeId";
 };
 
-// @beta @sealed (undocumented)
+// @beta @sealed
 export interface AttendeesEvents {
     // @eventProperty
     attendeeConnected: (attendee: Attendee) => void;
@@ -126,15 +126,12 @@ export interface LatestArguments<T extends object | null> {
 
 // @beta @sealed
 export interface LatestClientData<T> extends LatestData<T> {
-    // (undocumented)
     attendee: Attendee;
 }
 
 // @beta @sealed
 export interface LatestData<T> {
-    // (undocumented)
     metadata: LatestMetadata;
-    // (undocumented)
     value: DeepReadonly<JsonDeserialized<T>>;
 }
 
@@ -152,23 +149,18 @@ export interface LatestMapArguments<T, Keys extends string | number = string | n
 // @beta @sealed
 export interface LatestMapClientData<T, Keys extends string | number, SpecificAttendeeId extends AttendeeId = AttendeeId> {
     attendee: Attendee<SpecificAttendeeId>;
-    // (undocumented)
     items: ReadonlyMap<Keys, LatestData<T>>;
 }
 
 // @beta @sealed
 export interface LatestMapItemRemovedClientData<K extends string | number> {
-    // (undocumented)
     attendee: Attendee;
-    // (undocumented)
     key: K;
-    // (undocumented)
     metadata: LatestMetadata;
 }
 
 // @beta @sealed
 export interface LatestMapItemUpdatedClientData<T, K extends string | number> extends LatestClientData<T> {
-    // (undocumented)
     key: K;
 }
 
@@ -183,7 +175,7 @@ export interface LatestMapRaw<T, Keys extends string | number = string | number>
     readonly presence: Presence;
 }
 
-// @beta @sealed (undocumented)
+// @beta @sealed
 export interface LatestMapRawEvents<T, K extends string | number> {
     // @eventProperty
     localItemRemoved: (removedItem: {
@@ -220,7 +212,7 @@ export interface LatestRaw<T> {
     readonly presence: Presence;
 }
 
-// @beta @sealed (undocumented)
+// @beta @sealed
 export interface LatestRawEvents<T> {
     // @eventProperty
     localUpdated: (update: {
@@ -246,7 +238,7 @@ export interface Presence {
     };
 }
 
-// @beta @sealed (undocumented)
+// @beta @sealed
 export interface PresenceEvents {
     workspaceActivated: (workspaceAddress: WorkspaceAddress, type: "States" | "Notifications" | "Unknown") => void;
 }
@@ -260,15 +252,12 @@ export const StateFactory: {
 // @beta @sealed
 export interface StateMap<K extends string | number, V> {
     clear(): void;
-    // (undocumented)
     delete(key: K): boolean;
     forEach(callbackfn: (value: DeepReadonly<JsonDeserialized<V>>, key: K, map: StateMap<K, V>) => void, thisArg?: unknown): void;
     get(key: K): DeepReadonly<JsonDeserialized<V>> | undefined;
-    // (undocumented)
     has(key: K): boolean;
     keys(): IterableIterator<K>;
     set(key: K, value: JsonSerializable<V> & JsonDeserialized<V>): this;
-    // (undocumented)
     readonly size: number;
 }
 
@@ -293,7 +282,6 @@ export type StatesWorkspaceEntry<TKey extends string, TValue extends InternalTyp
 
 // @beta
 export interface StatesWorkspaceSchema {
-    // (undocumented)
     [key: string]: StatesWorkspaceEntry<typeof key, InternalTypes.ValueDirectoryOrState<any>>;
 }
 
diff --git a/packages/framework/presence/src/latestMapValueManager.ts b/packages/framework/presence/src/latestMapValueManager.ts
index 17e17bf6b8f6..b3a0c18c6e9c 100644
--- a/packages/framework/presence/src/latestMapValueManager.ts
+++ b/packages/framework/presence/src/latestMapValueManager.ts
@@ -23,7 +23,7 @@ import { datastoreFromHandle, type StateDatastore } from "./stateDatastore.js";
 import { brandIVM } from "./valueManager.js";
 
 /**
- * Collection of latest known values for a specific client.
+ * Collection of latest known values for a specific {@link Attendee}.
  *
  * @sealed
  * @beta
@@ -34,11 +34,13 @@ export interface LatestMapClientData<
 	SpecificAttendeeId extends AttendeeId = AttendeeId,
 > {
 	/**
-	 * Associated attendee.
+	 * Associated {@link Attendee}.
 	 */
 	attendee: Attendee<SpecificAttendeeId>;
 
 	/**
+	 * Map of items for the state.
+	 *
 	 * @privateRemarks This could be regular map currently as no Map is
 	 * stored internally and a new instance is created for every request.
 	 */
@@ -53,6 +55,9 @@ export interface LatestMapClientData<
  */
 export interface LatestMapItemUpdatedClientData<T, K extends string | number>
 	extends LatestClientData<T> {
+	/**
+	 * Key of the updated item.
+	 */
 	key: K;
 }
 
@@ -63,12 +68,23 @@ export interface LatestMapItemUpdatedClientData<T, K extends string | number>
  * @beta
  */
 export interface LatestMapItemRemovedClientData<K extends string | number> {
+	/**
+	 * Associated {@link Attendee}.
+	 */
 	attendee: Attendee;
+	/**
+	 * Key of the removed item.
+	 */
 	key: K;
+	/**
+	 * Metadata associated with the removal of the item.
+	 */
 	metadata: LatestMetadata;
 }
 
 /**
+ * Events from {@link LatestMapRaw}.
+ *
  * @sealed
  * @beta
  */
@@ -135,6 +151,8 @@ export interface StateMap<K extends string | number, V> {
 	clear(): void;
 
 	/**
+	 * Removes the element with the specified key from the StateMap, if it exists.
+	 *
 	 * @returns true if an element in the StateMap existed and has been removed, or false if
 	 * the element does not exist.
 	 * @remarks No entry is fully removed. Instead an undefined placeholder is locally and
@@ -158,12 +176,14 @@ export interface StateMap<K extends string | number, V> {
 	): void;
 
 	/**
-	 * Returns a specified element from the StateMap object.
+	 * Returns the element with the specified key from the StateMap, if it exists.
+	 *
 	 * @returns Returns the element associated with the specified key. If no element is associated with the specified key, undefined is returned.
 	 */
 	get(key: K): DeepReadonly<JsonDeserialized<V>> | undefined;
 
 	/**
+	 * Checks if an element with the specified key exists in the StateMap.
 	 * @returns boolean indicating whether an element with the specified key exists or not.
 	 */
 	has(key: K): boolean;
@@ -179,7 +199,7 @@ export interface StateMap<K extends string | number, V> {
 	set(key: K, value: JsonSerializable<V> & JsonDeserialized<V>): this;
 
 	/**
-	 * @returns the number of elements in the StateMap.
+	 * The number of elements in the StateMap.
 	 */
 	readonly size: number;
 
diff --git a/packages/framework/presence/src/latestValueManager.ts b/packages/framework/presence/src/latestValueManager.ts
index 090c6f8f8d4b..558307e62847 100644
--- a/packages/framework/presence/src/latestValueManager.ts
+++ b/packages/framework/presence/src/latestValueManager.ts
@@ -23,6 +23,8 @@ import { datastoreFromHandle, type StateDatastore } from "./stateDatastore.js";
 import { brandIVM } from "./valueManager.js";
 
 /**
+ * Events from {@link LatestRaw}.
+ *
  * @sealed
  * @beta
  */
diff --git a/packages/framework/presence/src/latestValueTypes.ts b/packages/framework/presence/src/latestValueTypes.ts
index 6b58d0c9f611..2c6e28920370 100644
--- a/packages/framework/presence/src/latestValueTypes.ts
+++ b/packages/framework/presence/src/latestValueTypes.ts
@@ -35,16 +35,27 @@ export interface LatestMetadata {
  * @beta
  */
 export interface LatestData<T> {
+	/**
+	 * The value of the state.
+	 * @remarks This is a deeply readonly value, meaning it cannot be modified.
+	 */
 	value: DeepReadonly<JsonDeserialized<T>>;
+
+	/**
+	 * Metadata associated with the value.
+	 */
 	metadata: LatestMetadata;
 }
 
 /**
- * State of a specific attendee's value and its metadata.
+ * State of a specific {@link Attendee}'s value and its metadata.
  *
  * @sealed
  * @beta
  */
 export interface LatestClientData<T> extends LatestData<T> {
+	/**
+	 * Associated {@link Attendee}.
+	 */
 	attendee: Attendee;
 }
diff --git a/packages/framework/presence/src/presence.ts b/packages/framework/presence/src/presence.ts
index f47412d078a3..5e4d6d74c9c1 100644
--- a/packages/framework/presence/src/presence.ts
+++ b/packages/framework/presence/src/presence.ts
@@ -116,6 +116,8 @@ export type SpecificAttendee<SpecificAttendeeId extends AttendeeId> =
 	string extends SpecificAttendeeId ? never : Attendee<SpecificAttendeeId>;
 
 /**
+ * Events from {@link Presence.attendees}.
+ *
  * @sealed
  * @beta
  */
@@ -136,6 +138,8 @@ export interface AttendeesEvents {
 }
 
 /**
+ * Events from {@link Presence}.
+ *
  * @sealed
  * @beta
  */
@@ -202,12 +206,12 @@ export interface Presence {
 
 	readonly states: {
 		/**
-		 * Acquires a StatesWorkspace from store or adds new one.
+		 * Acquires a {@link StatesWorkspace} from store or adds new one.
 		 *
-		 * @param workspaceAddress - Address of the requested StatesWorkspace
+		 * @param workspaceAddress - Address of the requested {@link StatesWorkspace}
 		 * @param requestedStates - Requested states for the workspace
 		 * @param controls - Optional settings for default broadcast controls
-		 * @returns A StatesWorkspace
+		 * @returns A {@link StatesWorkspace}
 		 */
 		getWorkspace<StatesSchema extends StatesWorkspaceSchema>(
 			workspaceAddress: WorkspaceAddress,
diff --git a/packages/framework/presence/src/types.ts b/packages/framework/presence/src/types.ts
index 1fa0590babee..5a3d64120be3 100644
--- a/packages/framework/presence/src/types.ts
+++ b/packages/framework/presence/src/types.ts
@@ -47,6 +47,9 @@ export type StatesWorkspaceEntry<
  * @beta
  */
 export interface StatesWorkspaceSchema {
+	/**
+	 * Key-value pairs of State objects registered with the {@link StatesWorkspace}.
+	 */
 	[key: string]: StatesWorkspaceEntry<typeof key, InternalTypes.ValueDirectoryOrState<any>>;
 }
 


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR adds inline documentation to various beta APIs in the Presence package, ensuring that key structures such as StatesWorkspaceSchema, Presence events, and Latest* types are properly documented. The changes update multiple files by inserting JSDoc comments that reference related entities via {@link} tags and by cleaning up undocumented annotations in API report files.
- Add documentation for key/value states in StatesWorkspaceSchema.
- Annotate event interfaces and methods in Presence and Latest* files.
- Remove "(undocumented)" placeholders in API report markdown files.

### Reviewed Changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated no comments.

<details>
<summary>Show a summary per file</summary>

| File | Description |
| ---- | ----------- |
| packages/framework/presence/src/types.ts | Added documentation for StatesWorkspaceSchema key-value pairs. |
| packages/framework/presence/src/presence.ts | Updated inline comments to reference {@link Presence.attendees} and {@link StatesWorkspace}. |
| packages/framework/presence/src/latestValueTypes.ts | Documented value and metadata properties with inline linking to {@link Attendee}. |
| packages/framework/presence/src/latestValueManager.ts | Added event documentation for {@link LatestRaw}. |
| packages/framework/presence/src/latestMapValueManager.ts | Enriched documentation for map value handling and associated types. |
| packages/framework/presence/api-report/presence.beta.api.md | Removed undocumented placeholders from beta API report. |
| packages/framework/presence/api-report/presence.alpha.api.md | Removed undocumented placeholders from alpha API report. |
</details>





state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T20:09:53Z
user: login: Josmithr
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T20:17:11Z
user: login: Josmithr
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T20:17:59Z
user: login: Josmithr
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T20:18:48Z
user: login: Josmithr
body: Thanks for the updates! Left a few minor suggestions.

> adds documentation to beta APIs that are not @system

While I agree with prioritizing adding docs to user-facing APIs, docs are important for our engineers (and the AI models we're training to replace us) too 🙂
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T20:21:22Z
user: login: jason-ha
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T20:34:19Z
user: login: jason-ha
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T20:36:30Z
user: login: jason-ha
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T20:41:49Z
user: login: Josmithr
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T21:22:59Z
user: login: Josmithr
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T21:25:38Z
user: login: Josmithr
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T21:26:22Z
user: login: noencke
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T22:48:08Z
user: login: tylerbutler
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-29T21:26:39Z
user: login: tylerbutler
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-29T21:27:09Z
user: login: Josmithr
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-29T21:44:49Z
user: login: jason-ha
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-29T21:55:41Z
user: login: Josmithr
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-30T17:14:31Z

Review comments:
diff_hunk: @@ -53,6 +55,9 @@ export interface LatestMapClientData<
  */
 export interface LatestMapItemUpdatedClientData<T, K extends string | number>
 	extends LatestClientData<T> {
+	/**
+	 * Key of the updated item.
+	 */
 	key: K;
path: packages/framework/presence/src/latestMapValueManager.ts
user: login: Josmithr
body: Unrelated: should this (and other interface properties in this module) be readonly?
created_at: 2025-05-28T20:17:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -158,12 +176,14 @@ export interface StateMap<K extends string | number, V> {
 	): void;
 
 	/**
-	 * Returns a specified element from the StateMap object.
+	 * Returns the element with the specified key from the StateMap.
path: packages/framework/presence/src/latestMapValueManager.ts
user: login: Josmithr
body: Nit
```suggestion
	 * Returns the element with the specified key from the StateMap, if it exists.
```
?
created_at: 2025-05-28T20:17:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -179,6 +199,8 @@ export interface StateMap<K extends string | number, V> {
 	set(key: K, value: JsonSerializable<V> & JsonDeserialized<V>): this;
 
 	/**
+	 * The number of elements in the StateMap.
path: packages/framework/presence/src/latestMapValueManager.ts
user: login: Josmithr
body: Nit: it would probably be good to remove the `@returns` block, especially since this is a property.
created_at: 2025-05-28T20:18:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -53,6 +55,9 @@ export interface LatestMapClientData<
  */
 export interface LatestMapItemUpdatedClientData<T, K extends string | number>
 	extends LatestClientData<T> {
+	/**
+	 * Key of the updated item.
+	 */
 	key: K;
path: packages/framework/presence/src/latestMapValueManager.ts
user: login: jason-ha
body: Probably could be. There isn't a need as these are constructed on demand and handed out.
created_at: 2025-05-28T20:34:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -158,12 +176,14 @@ export interface StateMap<K extends string | number, V> {
 	): void;
 
 	/**
-	 * Returns a specified element from the StateMap object.
+	 * Returns the element with the specified key from the StateMap.
path: packages/framework/presence/src/latestMapValueManager.ts
user: login: jason-ha
body: The funny thing with all of these docs is that this is a clone of `Map`. So all of the comments before this change are the exact docs from `Map` with `StateMap` injected in places. :)
created_at: 2025-05-28T20:36:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -179,6 +199,8 @@ export interface StateMap<K extends string | number, V> {
 	set(key: K, value: JsonSerializable<V> & JsonDeserialized<V>): this;
 
 	/**
+	 * The number of elements in the StateMap.
path: packages/framework/presence/src/latestMapValueManager.ts
user: login: jason-ha
body: Note: readonly "properties" may be implemented as getters. That is the case here.
It feels a little unfortunate that api-extractor complains about `@returns` only docs.
Will remove the `@returns` block.
created_at: 2025-05-28T20:41:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -179,6 +199,8 @@ export interface StateMap<K extends string | number, V> {
 	set(key: K, value: JsonSerializable<V> & JsonDeserialized<V>): this;
 
 	/**
+	 * The number of elements in the StateMap.
path: packages/framework/presence/src/latestMapValueManager.ts
user: login: Josmithr
body: We've _opted into_ eslint requirements for summary docs. This makes the user-facing presentation of the generated API docs on the website better.

We _could_ update the website build to try to derive summary docs from `@returns` blocks when the summary is missing, but the way devs write `@returns` comments is very different from how they write general descriptions. This makes it hard to come up with a consistent strategy for transforming `@returns` comments into a general summary.

So, we prefer summary comments first, and `@returns` for adding additional context about the return value (when there is more to say than what is captured in the summary).
created_at: 2025-05-28T21:22:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -179,6 +199,8 @@ export interface StateMap<K extends string | number, V> {
 	set(key: K, value: JsonSerializable<V> & JsonDeserialized<V>): this;
 
 	/**
+	 * The number of elements in the StateMap.
path: packages/framework/presence/src/latestMapValueManager.ts
user: login: Josmithr
body: And in terms of getter properties: the user shouldn't really be aware that the property they're looking at is backed by a getter. It's just a property. If they need to be aware of this distinction for whatever reason, then it should really be a method.
created_at: 2025-05-28T21:25:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -53,6 +55,9 @@ export interface LatestMapClientData<
  */
 export interface LatestMapItemUpdatedClientData<T, K extends string | number>
 	extends LatestClientData<T> {
+	/**
+	 * Key of the updated item.
+	 */
 	key: K;
path: packages/framework/presence/src/latestMapValueManager.ts
user: login: Josmithr
body: Easier to remove `readonly` than to add it in the future, so I've generally been recommending using it when possible.
created_at: 2025-05-28T21:26:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -53,6 +55,9 @@ export interface LatestMapClientData<
  */
 export interface LatestMapItemUpdatedClientData<T, K extends string | number>
 	extends LatestClientData<T> {
+	/**
+	 * Key of the updated item.
+	 */
 	key: K;
path: packages/framework/presence/src/latestMapValueManager.ts
user: login: noencke
body: My two cents - when applied to an object that is created solely for the purpose of being returned to a caller (as Jason was saying), `readonly` at best adds no value and at worst is annoying to the caller because they need to copy the object to mutate it (but they already _have_ a copy).

On the other hand, for objects being used as input or for objects shared by multiple owners, `readonly` should be used as liberally as possible, i.e. everywhere unless there's a reason not to.
created_at: 2025-05-28T22:48:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -53,6 +55,9 @@ export interface LatestMapClientData<
  */
 export interface LatestMapItemUpdatedClientData<T, K extends string | number>
 	extends LatestClientData<T> {
+	/**
+	 * Key of the updated item.
+	 */
 	key: K;
path: packages/framework/presence/src/latestMapValueManager.ts
user: login: tylerbutler
body: Do we have some coding guidelines that we could put these in? @noencke's description seems super helpful.
created_at: 2025-05-29T21:26:38Z
author_association: MEMBER
diff_hunk: @@ -53,6 +55,9 @@ export interface LatestMapClientData<
  */
 export interface LatestMapItemUpdatedClientData<T, K extends string | number>
 	extends LatestClientData<T> {
+	/**
+	 * Key of the updated item.
+	 */
 	key: K;
path: packages/framework/presence/src/latestMapValueManager.ts
user: login: Josmithr
body: No, but we should. 

We do now have tags to differentiate conceptual input ([@input](https://github.com/microsoft/FluidFramework/wiki/TSDoc-Guidelines#input)) vs output ([@sealed](https://github.com/microsoft/FluidFramework/wiki/TSDoc-Guidelines#sealed)) types, though, which are related (properties of input types should generally be `readonly`, and properties of output types generally shouldn't).

Having both kinds of tags should help make it more obvious in code reviews which is more appropriate.
created_at: 2025-05-29T21:44:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -53,6 +55,9 @@ export interface LatestMapClientData<
  */
 export interface LatestMapItemUpdatedClientData<T, K extends string | number>
 	extends LatestClientData<T> {
+	/**
+	 * Key of the updated item.
+	 */
 	key: K;
path: packages/framework/presence/src/latestMapValueManager.ts
user: login: jason-ha
body: Fun TypeScript fact - immutability of an object can be removed just by simple assignment to variable that doesn't have readonly attribute.
I will say there are certainly cases for all of the variations. And out type might be expensive to copy deeply; so, it makes sense to share as deeply readonly and let the consumer clone if they want to mutate.
And a large input might not want to be readonly for similar reason. The provider gets to decide if clone is needed when the API asks for something mutable.

Ew, `@input` - when did that show up?  Oh yesterday :)
created_at: 2025-05-29T21:55:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -53,6 +55,9 @@ export interface LatestMapClientData<
  */
 export interface LatestMapItemUpdatedClientData<T, K extends string | number>
 	extends LatestClientData<T> {
+	/**
+	 * Key of the updated item.
+	 */
 	key: K;
path: packages/framework/presence/src/latestMapValueManager.ts
user: login: Josmithr
body: > I will say there are certainly cases for all of the variations. And out type might be expensive to copy deeply; so, it makes sense to share as deeply readonly and let the consumer clone if they want to mutate.
And a large input might not want to be readonly for similar reason. The provider gets to decide if clone is needed when the API asks for something mutable.

There will always be exceptions, but generally speaking I think the guidance holds: 
- We don't want to accidentally mutate user input, so we make the properties readonly.
- We don't want to burden the user by making output properties immutable, so we don't.
created_at: 2025-05-30T17:14:30Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-28T21:04:53Z
body: 🔗 No broken links found! ✅

Your attention to detail is admirable.

#### linkcheck output

```

> fluid-framework-docs-site@0.0.0 ci:check-links /home/runner/work/FluidFramework/FluidFramework/docs
> start-server-and-test "npm run serve -- --no-open" 3000 check-links

1: starting server using command "npm run serve -- --no-open"
and when url "[ 'http://127.0.0.1:3000' ]" is responding with HTTP status code 200
running tests using command "npm run check-links"


> fluid-framework-docs-site@0.0.0 serve
> docusaurus serve --no-open

[SUCCESS] Serving "build" directory at: http://localhost:3000/

> fluid-framework-docs-site@0.0.0 check-links
> linkcheck http://localhost:3000 --skip-file skipped-urls.txt

Crawling...

Stats:
  209835 links
    1663 destination URLs
    1895 URLs ignored
       0 warnings
       0 errors


```

<!-- Sticky Pull Request Commentlinkreport -->
author_association: CONTRIBUTOR