Bump protocol to v9 and carry talk-circle ids on the presence frame.

Names stay off the wire; a person not in a circle writes an empty member list and topic.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leonid Pershin
2026-08-20 10:42:11 +03:00
co-authored by Cursor
parent 52a4104f68
commit 1a21a64b4b
7 changed files with 321 additions and 10 deletions
+124 -2
View File
@@ -262,7 +262,9 @@ describe('decodeServerMessage', () => {
+ 2
+ 2 + personId.length
+ 2 + personNode.length
+ 1,
+ 1
+ 1
+ 2,
);
const view = new DataView(buffer);
view.setUint8(0, MessageType.ServerPresence);
@@ -296,6 +298,10 @@ describe('decodeServerMessage', () => {
new Uint8Array(buffer).set(personNode, offset);
offset += personNode.length;
view.setUint8(offset, PresenceState.Here);
offset += 1;
view.setUint8(offset, 0);
offset += 1;
view.setUint16(offset, 0, true);
expect(decodeServerMessage(buffer)).toEqual({
type: 'presence',
@@ -308,7 +314,123 @@ describe('decodeServerMessage', () => {
activityClass: '5А',
},
],
people: [{ id: 'f0.c0', nodeId: 'classroom-101', state: PresenceState.Here }],
people: [
{
id: 'f0.c0',
nodeId: 'classroom-101',
state: PresenceState.Here,
talkMemberIds: [],
talkTopicId: '',
},
],
});
});
it('reads a presence person with talk-circle member ids and topic id', () => {
const encoder = new TextEncoder();
const putString = (target: DataView, at: number, text: Uint8Array): number => {
target.setUint16(at, text.length, true);
new Uint8Array(target.buffer).set(text, at + 2);
return at + 2 + text.length;
};
const corridor = encoder.encode('corridor-1');
const selfId = encoder.encode('f0.c0');
const partnerId = encoder.encode('f0.c1');
const topic = encoder.encode('TopicSport');
const buffer = new ArrayBuffer(
7
+ 2
+ 2
+ 2 + selfId.length
+ 2 + corridor.length
+ 1
+ 1
+ 2 + selfId.length
+ 2 + partnerId.length
+ 2 + topic.length,
);
const view = new DataView(buffer);
view.setUint8(0, MessageType.ServerPresence);
view.setInt32(1, 1, true);
view.setUint16(5, 0, true);
view.setUint16(7, 1, true);
let offset = 9;
offset = putString(view, offset, selfId);
offset = putString(view, offset, corridor);
view.setUint8(offset, PresenceState.Here);
offset += 1;
view.setUint8(offset, 2);
offset += 1;
offset = putString(view, offset, selfId);
offset = putString(view, offset, partnerId);
putString(view, offset, topic);
expect(decodeServerMessage(buffer)).toEqual({
type: 'presence',
schoolId: 1,
nodes: [],
people: [
{
id: 'f0.c0',
nodeId: 'corridor-1',
state: PresenceState.Here,
talkMemberIds: ['f0.c0', 'f0.c1'],
talkTopicId: 'TopicSport',
},
],
});
});
it('does not carry a person display name in a presence frame', () => {
const encoder = new TextEncoder();
const putString = (target: DataView, at: number, text: Uint8Array): number => {
target.setUint16(at, text.length, true);
new Uint8Array(target.buffer).set(text, at + 2);
return at + 2 + text.length;
};
const corridor = encoder.encode('corridor-1');
const selfId = encoder.encode('f0.c0');
const topic = encoder.encode('TopicSport');
const buffer = new ArrayBuffer(
7 + 2 + 2 + selfId.length + 2 + corridor.length + 1 + 1 + 2 + selfId.length + 2 + topic.length,
);
const view = new DataView(buffer);
view.setUint8(0, MessageType.ServerPresence);
view.setInt32(1, 1, true);
view.setUint16(5, 0, true);
view.setUint16(7, 1, true);
let offset = 9;
offset = putString(view, offset, selfId);
offset = putString(view, offset, corridor);
view.setUint8(offset, PresenceState.Here);
offset += 1;
view.setUint8(offset, 1);
offset += 1;
offset = putString(view, offset, selfId);
putString(view, offset, topic);
const bytes = new Uint8Array(buffer);
const text = new TextDecoder().decode(bytes);
expect(text).not.toContain('Мария');
expect(text).not.toContain('Иванова');
expect(text).not.toContain('Маша');
expect(text).toContain('f0.c0');
expect(text).toContain('TopicSport');
expect(decodeServerMessage(buffer)).toEqual({
type: 'presence',
schoolId: 1,
nodes: [],
people: [
{
id: 'f0.c0',
nodeId: 'corridor-1',
state: PresenceState.Here,
talkMemberIds: ['f0.c0'],
talkTopicId: 'TopicSport',
},
],
});
});
+21 -2
View File
@@ -5,7 +5,7 @@
* changed together and documented in `docs/protocol.md`. All numbers are little-endian.
*/
export const PROTOCOL_VERSION = 8;
export const PROTOCOL_VERSION = 9;
export const MessageType = {
ClientHello: 0x01,
@@ -122,6 +122,8 @@ export interface PresencePerson {
readonly id: string;
readonly nodeId: string;
readonly state: number;
readonly talkMemberIds: readonly string[];
readonly talkTopicId: string;
}
export interface PresenceMessage {
@@ -375,7 +377,24 @@ function decodePresence(view: DataView): PresenceMessage {
offset = nodeId.next;
const state = readU8(view, offset);
offset += 1;
people.push({ id: id.text, nodeId: nodeId.text, state });
const memberCount = readU8(view, offset);
offset += 1;
const talkMemberIds: string[] = [];
for (let member = 0; member < memberCount; member++) {
const memberId = readString(view, offset);
offset = memberId.next;
talkMemberIds.push(memberId.text);
}
const topic = readString(view, offset);
offset = topic.next;
people.push({
id: id.text,
nodeId: nodeId.text,
state,
talkMemberIds,
talkTopicId: topic.text,
});
}
return { type: 'presence', schoolId, nodes, people };