feat(workspace): add real-time live cursor and presence tracking
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 56s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 56s
- Support ephemeral cursor_moved and client_left events in docroom Hub - Keep active in-memory cursor presence map per document room - Synchronize current presence state to newly connected WebSocket clients - Allow read-only viewers to share cursor positions without document mutation rights - Throttle frontend cursor move emissions to 100ms with instant blur cleanup - Render active collaborator presence avatar pills in document header - Display user-colored block indicator and non-destructive caret overlay with name tag
This commit is contained in:
1 parent
85e4010435
commit
95977ba15a
7 files changed
+643
-68
No files matched your search
@@ -0,0 +1,85 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestDocRoomCursorPresenceAndEchoProtection(t *testing.T) {
|
||||
hub := NewHub()
|
||||
room := hub.GetOrCreateDocRoom("doc_test_presence")
|
||||
|
||||
c1 := &DocWSClient{
|
||||
hub: hub,
|
||||
room: room,
|
||||
userID: "user_1",
|
||||
clientID: "cli_1",
|
||||
canWrite: true,
|
||||
send: make(chan []byte, 10),
|
||||
}
|
||||
c2 := &DocWSClient{
|
||||
hub: hub,
|
||||
room: room,
|
||||
userID: "user_2",
|
||||
clientID: "cli_2",
|
||||
canWrite: false, // read-only viewer
|
||||
send: make(chan []byte, 10),
|
||||
}
|
||||
|
||||
room.mu.Lock()
|
||||
room.wsClients[c1] = true
|
||||
room.wsClients[c2] = true
|
||||
room.mu.Unlock()
|
||||
|
||||
// 1. Send cursor_moved from c1
|
||||
selStart := 5
|
||||
selEnd := 5
|
||||
room.HandleEvent(DocSyncEvent{
|
||||
Type: "cursor_moved",
|
||||
DocID: "doc_test_presence",
|
||||
ClientID: "cli_1",
|
||||
UserID: "user_1",
|
||||
UserName: "Alice",
|
||||
UserColor: "#3b82f6",
|
||||
BlockID: "blk_123",
|
||||
SelectionStart: &selStart,
|
||||
SelectionEnd: &selEnd,
|
||||
}, c1)
|
||||
|
||||
// c1 should NOT have received its own echo
|
||||
select {
|
||||
case <-c1.send:
|
||||
t.Fatal("c1 received echo of its own cursor_moved event!")
|
||||
default:
|
||||
}
|
||||
|
||||
// c2 SHOULD receive the event
|
||||
select {
|
||||
case msg := <-c2.send:
|
||||
if len(msg) == 0 {
|
||||
t.Fatal("empty message received by c2")
|
||||
}
|
||||
case <-time.After(500 * time.Millisecond):
|
||||
t.Fatal("timed out waiting for cursor_moved event on c2")
|
||||
}
|
||||
|
||||
// Room should have registered c1's presence
|
||||
room.mu.Lock()
|
||||
cp, exists := room.activeCursors["cli_1"]
|
||||
room.mu.Unlock()
|
||||
|
||||
if !exists || cp == nil {
|
||||
t.Fatal("expected active cursor for cli_1 in room")
|
||||
}
|
||||
if cp.UserName != "Alice" || cp.BlockID != "blk_123" {
|
||||
t.Fatalf("unexpected cursor data: %+v", cp)
|
||||
}
|
||||
|
||||
// Ephemeral cursor events must NOT mark the room dirty for disk flush
|
||||
room.mu.Lock()
|
||||
dirty := room.dirty
|
||||
room.mu.Unlock()
|
||||
if dirty {
|
||||
t.Fatal("cursor_moved should not mark room dirty")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user