Skip to content

Commit e28c0de

Browse files
authored
add docs for presence, node api (liveblocks#3068)
1 parent e2f85e4 commit e28c0de

File tree

5 files changed

+244
-0
lines changed

5 files changed

+244
-0
lines changed

docs/pages/api-reference/liveblocks-node.mdx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1157,6 +1157,37 @@ useEventListener(({ event, user, connectionId }) => {
11571157
});
11581158
```
11591159

1160+
#### Liveblocks.setPresence [#post-rooms-roomId-presence]
1161+
1162+
Sets ephemeral presence for a user in a room without requiring a WebSocket
1163+
connection. The presence data automatically expires after the specified TTL
1164+
(time-to-live). This is useful for scenarios like showing an AI agent’s presence
1165+
in a room. The presence is broadcast to all connected users in the room. This is
1166+
a wrapper around the
1167+
[Set Presence API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-presence)
1168+
and returns no response on success.
1169+
1170+
```ts
1171+
await liveblocks.setPresence("my-room-id", {
1172+
userId: "agent-123",
1173+
data: {
1174+
status: "active",
1175+
cursor: { x: 100, y: 200 },
1176+
},
1177+
userInfo: {
1178+
name: "AI Assistant",
1179+
avatar: "https://example.com/avatar.png",
1180+
},
1181+
ttl: 60, // optional, 2–3599 seconds
1182+
});
1183+
```
1184+
1185+
- **userId** (required): The ID of the user to set presence for.
1186+
- **data** (required): Presence data as a JSON object.
1187+
- **userInfo** (optional): Metadata about the user or agent
1188+
- **ttl** (optional): Time-to-live in seconds (minimum 2, maximum 3599).
1189+
Defaults to 60. After this duration, the presence expires automatically.
1190+
11601191
### Groups
11611192

11621193
Groups allow you to manage users for group mentions in comments and text

docs/references/v2.openapi.json

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -757,6 +757,71 @@
757757
}
758758
}
759759
},
760+
"/rooms/{roomId}/presence": {
761+
"post": {
762+
"summary": "Set ephemeral presence",
763+
"description": "This endpoint sets ephemeral presence for a user in a room without requiring a WebSocket connection. The presence data will automatically expire after the specified TTL (time-to-live). This is useful for scenarios like showing an AI agent's presence in a room. The presence will be broadcast to all connected users in the room.",
764+
"tags": ["Room"],
765+
"operationId": "post-rooms-roomId-presence",
766+
"parameters": [
767+
{
768+
"schema": {
769+
"type": "string"
770+
},
771+
"name": "roomId",
772+
"in": "path",
773+
"required": true,
774+
"description": "ID of the room"
775+
}
776+
],
777+
"requestBody": {
778+
"required": true,
779+
"content": {
780+
"application/json": {
781+
"schema": {
782+
"$ref": "#/components/schemas/SetPresence"
783+
},
784+
"examples": {
785+
"example": {
786+
"value": {
787+
"userId": "agent-123",
788+
"data": {
789+
"status": "active",
790+
"cursor": {
791+
"x": 100,
792+
"y": 200
793+
}
794+
},
795+
"userInfo": {
796+
"name": "AI Assistant",
797+
"avatar": "https://example.org/images/agent123.jpg"
798+
},
799+
"ttl": 60
800+
}
801+
}
802+
}
803+
}
804+
}
805+
},
806+
"responses": {
807+
"204": {
808+
"description": "Success. Presence was set for the user in the room."
809+
},
810+
"401": {
811+
"$ref": "#/components/responses/401"
812+
},
813+
"403": {
814+
"$ref": "#/components/responses/403"
815+
},
816+
"404": {
817+
"$ref": "#/components/responses/404"
818+
},
819+
"422": {
820+
"$ref": "#/components/responses/422"
821+
}
822+
}
823+
}
824+
},
760825
"/rooms/{roomId}/broadcast_event": {
761826
"post": {
762827
"summary": "Broadcast event to a room",
@@ -8513,6 +8578,47 @@
85138578
}
85148579
},
85158580
"required": ["message"]
8581+
},
8582+
"SetPresence": {
8583+
"title": "SetPresence",
8584+
"type": "object",
8585+
"properties": {
8586+
"userId": {
8587+
"type": "string",
8588+
"description": "ID of the user to set presence for"
8589+
},
8590+
"data": {
8591+
"type": "object",
8592+
"description": "Presence data as a JSON object"
8593+
},
8594+
"userInfo": {
8595+
"type": "object",
8596+
"properties": {
8597+
"name": {
8598+
"type": "string",
8599+
"minLength": 1,
8600+
"description": "Optional name for the user or agent"
8601+
},
8602+
"avatar": {
8603+
"type": "string",
8604+
"description": "Optional avatar URL for the user"
8605+
},
8606+
"color": {
8607+
"type": "string",
8608+
"description": "Optional color for the user"
8609+
}
8610+
},
8611+
"required": [],
8612+
"description": "Metadata about the user or agent"
8613+
},
8614+
"ttl": {
8615+
"type": "integer",
8616+
"minimum": 2,
8617+
"maximum": 3599,
8618+
"description": "Time-to-live in seconds (minimum: 2, maximum: 3599). After this duration, the presence will automatically expire."
8619+
}
8620+
},
8621+
"required": ["userId", "data"]
85168622
}
85178623
},
85188624
"securitySchemes": {

packages/liveblocks-node/src/__tests__/client.test.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,9 @@ describe("client", () => {
118118
}),
119119
http.get(`${DEFAULT_BASE_URL}/v2/rooms/:roomId/prewarm`, () => {
120120
return new HttpResponse(null, { status: 204 });
121+
}),
122+
http.post(`${DEFAULT_BASE_URL}/v2/rooms/:roomId/presence`, () => {
123+
return new HttpResponse(null, { status: 204 });
121124
})
122125
);
123126

@@ -627,6 +630,65 @@ describe("client", () => {
627630
});
628631
});
629632

633+
describe("set presence", () => {
634+
test("should successfully set presence when receiving 204 response", async () => {
635+
const client = new Liveblocks({ secret: "sk_xxx" });
636+
637+
await expect(
638+
client.setPresence("room-123", {
639+
userId: "agent-ai",
640+
data: { status: "active", cursor: { x: 100, y: 200 } },
641+
userInfo: {
642+
name: "AI Assistant",
643+
avatar: "https://example.com/avatar.png",
644+
},
645+
ttl: 60,
646+
}),
647+
).resolves.toBeUndefined();
648+
});
649+
650+
test("should handle optional ttl parameter", async () => {
651+
const client = new Liveblocks({ secret: "sk_xxx" });
652+
653+
await expect(
654+
client.setPresence("room-123", {
655+
userId: "agent-ai",
656+
data: { status: "active" },
657+
userInfo: { name: "AI Assistant" },
658+
}),
659+
).resolves.toBeUndefined();
660+
});
661+
662+
test("should throw LiveblocksError on failure", async () => {
663+
server.use(
664+
http.post(`${DEFAULT_BASE_URL}/v2/rooms/:roomId/presence`, () => {
665+
return HttpResponse.json(
666+
{ error: "INVALID_REQUEST", message: "Invalid presence data" },
667+
{ status: 422 },
668+
);
669+
}),
670+
);
671+
672+
const client = new Liveblocks({ secret: "sk_xxx" });
673+
674+
try {
675+
await client.setPresence("room-123", {
676+
userId: "agent-ai",
677+
data: { status: "active" },
678+
userInfo: { name: "AI Assistant" },
679+
});
680+
expect(true).toBe(false);
681+
} catch (err) {
682+
expect(err instanceof LiveblocksError).toBe(true);
683+
if (err instanceof LiveblocksError) {
684+
expect(err.status).toBe(422);
685+
expect(err.message).toBe("Invalid presence data");
686+
expect(err.name).toBe("LiveblocksError");
687+
}
688+
}
689+
});
690+
});
691+
630692
describe("edit comment", () => {
631693
test("should return the edited comment when editComment receives a successful response", async () => {
632694
const commentData = {

packages/liveblocks-node/src/client.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -634,6 +634,18 @@ export type RequestOptions = {
634634
signal?: AbortSignal;
635635
};
636636

637+
export type SetPresenceOptions = {
638+
userId: string;
639+
data: JsonObject;
640+
userInfo?: {
641+
name?: string;
642+
avatar?: string;
643+
color?: string;
644+
[key: string]: Json | undefined;
645+
};
646+
ttl?: number;
647+
};
648+
637649
/**
638650
* Converts ISO-formatted date strings to Date instances on RoomDataPlain
639651
* values.
@@ -1219,6 +1231,38 @@ export class Liveblocks {
12191231
}
12201232
}
12211233

1234+
/**
1235+
* Sets ephemeral presence for a user in a room without requiring a WebSocket connection.
1236+
* The presence data will automatically expire after the specified TTL.
1237+
* This is useful for scenarios like showing an AI agent's presence in a room.
1238+
*
1239+
* @param roomId The id of the room to set presence in.
1240+
* @param params.userId The ID of the user to set presence for.
1241+
* @param params.data The presence data as a JSON object.
1242+
* @param params.userInfo (optional) Metadata about the user or agent
1243+
* @param params.ttl (optional) Time-to-live in seconds. If not specified, the default TTL is 60 seconds. (minimum: 2, maximum: 3599).
1244+
* @param options.signal (optional) An abort signal to cancel the request.
1245+
*/
1246+
public async setPresence(
1247+
roomId: string,
1248+
params: SetPresenceOptions,
1249+
options?: RequestOptions
1250+
): Promise<void> {
1251+
const res = await this.#post(
1252+
url`/v2/rooms/${roomId}/presence`,
1253+
{
1254+
userId: params.userId,
1255+
data: params.data,
1256+
userInfo: params.userInfo,
1257+
ttl: params.ttl,
1258+
},
1259+
options
1260+
);
1261+
if (!res.ok) {
1262+
throw await LiveblocksError.from(res);
1263+
}
1264+
}
1265+
12221266
/* -------------------------------------------------------------------------------------------------
12231267
* Storage
12241268
* -----------------------------------------------------------------------------------------------*/

packages/liveblocks-node/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export type {
2929
RoomPermission,
3030
RoomsQueryCriteria,
3131
RoomUser,
32+
SetPresenceOptions,
3233
ThreadParticipants,
3334
UpdateAiCopilotOptions,
3435
UpdateRoomOptions,

0 commit comments

Comments
 (0)