diff --git a/request_test.go b/request_test.go index a6b2c597..a8833caf 100644 --- a/request_test.go +++ b/request_test.go @@ -201,6 +201,48 @@ func TestRequest_DeleteElements(t *testing.T) { } } +func TestRequest_BroadcastDeleteRetainsFrames(t *testing.T) { + jw, err := New() + if err != nil { + t.Fatal(err) + } + defer jw.Close() + rq := &Request{Jaws: jw, tagMap: make(map[any][]*Element)} + target := tag.Tag("broadcast-delete") + deleted := []*Element{ + rq.NewElement(&testUi{}), + rq.NewElement(&testUi{}), + rq.NewElement(&testUi{}), + } + for _, elem := range deleted { + elem.Tag(target) + } + keep := rq.NewElement(&testUi{}) + rq.queue(wire.WsMsg{Jid: deleted[0].Jid(), What: what.Inner, Data: "stale"}) + + rq.handleBroadcast(wire.Message{Dest: target, What: what.Delete}, nil) + + for _, elem := range deleted { + if !elem.Deleted() || rq.GetElementByJid(elem.Jid()) != nil { + t.Errorf("element %v remains registered", elem.Jid()) + } + } + if got := rq.GetElements(target); len(got) != 0 { + t.Errorf("deleted tag still has %d elements", len(got)) + } + if got := rq.GetElementByJid(keep.Jid()); got != keep { + t.Errorf("untargeted element = %p, want %p", got, keep) + } + want := []wire.WsMsg{ + {Jid: deleted[0].Jid(), What: what.Delete}, + {Jid: deleted[1].Jid(), What: what.Delete}, + {Jid: deleted[2].Jid(), What: what.Delete}, + } + if got := rq.getSendMsgs(); !reflect.DeepEqual(got, want) { + t.Fatalf("outbound messages = %+v, want %+v", got, want) + } +} + func TestRequest_Registrations(t *testing.T) { is := newTestHelper(t) rq := newTestRequest(t) diff --git a/requestloop.go b/requestloop.go index d33fd2d7..9ebaf9ea 100644 --- a/requestloop.go +++ b/requestloop.go @@ -165,6 +165,9 @@ func (rq *Request) handleBroadcast(tagmsg wire.Message, eventCallCh chan eventFn default: todo = rq.GetElements(v) } + if tagmsg.What == what.Delete && len(todo) > 0 { + rq.DeleteElements(todo) + } for _, elem := range todo { switch tagmsg.What { @@ -173,7 +176,6 @@ func (rq *Request) handleBroadcast(tagmsg wire.Message, eventCallCh chan eventFn Jid: elem.Jid(), What: what.Delete, }) - rq.DeleteElement(elem) case what.Input, what.Click, what.ContextMenu: // Input, Click or ContextMenu messages received here come from broadcasts; // primarily used in tests by injecting a wire.WsMsg on the inbound channel. diff --git a/requestloop_remove_benchmark_test.go b/requestloop_remove_benchmark_test.go index 71c05b4c..31381531 100644 --- a/requestloop_remove_benchmark_test.go +++ b/requestloop_remove_benchmark_test.go @@ -1,6 +1,7 @@ package jaws import ( + "slices" "strconv" "strings" "testing" @@ -79,3 +80,26 @@ func BenchmarkRequestIncomingRemoveCleanup(b *testing.B) { }) } } + +func BenchmarkRequestBroadcastDelete(b *testing.B) { + const total, matched = 1000, 100 + rq := newBenchRequest(b, total) + rq.tagMap = make(map[any][]*Element) + all := slices.Clone(rq.elems) + targets := slices.Clone(all[:matched]) + dest := tag.Tag("broadcast-delete") + msg := wire.Message{Dest: dest, What: what.Delete} + b.ReportAllocs() + for range b.N { + b.StopTimer() + rq.elems = slices.Clone(all) + rq.tagMap[dest] = slices.Clone(targets) + for _, elem := range targets { + elem.deleted.Store(false) + } + rq.wsQueue = rq.wsQueue[:0] + b.StartTimer() + rq.handleBroadcast(msg, nil) + b.StopTimer() + } +}