From 2a703b08668164f8be6cfe6f8a2ead2ad2cb94f9 Mon Sep 17 00:00:00 2001 From: Paul Wells Date: Wed, 11 Mar 2026 21:22:42 -0700 Subject: [PATCH 1/2] add generic list type --- utils/list/list.go | 124 ++++++++++++++++++++++++++++++++++++++++ utils/list/list_test.go | 80 ++++++++++++++++++++++++++ 2 files changed, 204 insertions(+) create mode 100644 utils/list/list.go create mode 100644 utils/list/list_test.go diff --git a/utils/list/list.go b/utils/list/list.go new file mode 100644 index 000000000..6465158f3 --- /dev/null +++ b/utils/list/list.go @@ -0,0 +1,124 @@ +package list + +type Hook[T any] struct { + next *T + prev *T +} + +func (h *Hook[T]) getListHook() *Hook[T] { + return h +} + +func (h *Hook[T]) Next() *T { + if h == nil { + return nil + } + return h.next +} + +func (h *Hook[T]) Prev() *T { + if h == nil { + return nil + } + return h.prev +} + +type hookAccessor[T any] interface { + getListHook() *Hook[T] +} + +type Hooked[T any] interface { + *T + hookAccessor[T] +} + +type List[T any, P Hooked[T]] struct { + head P + tail P +} + +func (l *List[T, P]) Empty() bool { + return l.head == nil +} + +func (l *List[T, P]) Front() P { + return l.head +} + +func (l *List[T, P]) Back() P { + return l.tail +} + +func (l *List[T, P]) PushFront(it P) { + l.insert(it, nil, l.head) +} + +func (l *List[T, P]) PushBack(it P) { + l.insert(it, l.tail, nil) +} + +func (l *List[T, P]) InsertBefore(it, mark P) { + l.insert(it, mark.getListHook().prev, mark) +} + +func (l *List[T, P]) InsertAfter(it, mark P) { + l.insert(it, mark, mark.getListHook().next) +} + +func (l *List[T, P]) MoveToFront(it P) { + if l.head == it { + return + } + h := it.getListHook() + l.unlink(h) + l.link(it, h, nil, l.head) +} + +func (l *List[T, P]) MoveToBack(it P) { + if l.tail == it { + return + } + h := it.getListHook() + l.unlink(h) + l.link(it, h, l.tail, nil) +} + +func (l *List[T, P]) Remove(it P) { + h := it.getListHook() + l.unlink(h) + h.next = nil + h.prev = nil +} + +func (l *List[T, P]) insert(it, prev, next P) { + l.link(it, it.getListHook(), prev, next) +} + +func (l *List[T, P]) link(it P, h *Hook[T], prev, next P) { + h.prev = prev + h.next = next + + if prev != nil { + prev.getListHook().next = it + } else { + l.head = it + } + if next != nil { + next.getListHook().prev = it + } else { + l.tail = it + } +} + +func (l *List[T, P]) unlink(h *Hook[T]) { + if h.prev != nil { + P(h.prev).getListHook().next = h.next + } else { + l.head = h.next + } + if h.next != nil { + P(h.next).getListHook().prev = h.prev + } else { + l.tail = h.prev + } +} diff --git a/utils/list/list_test.go b/utils/list/list_test.go new file mode 100644 index 000000000..00c4fb7a3 --- /dev/null +++ b/utils/list/list_test.go @@ -0,0 +1,80 @@ +package list + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +type testItem struct { + Hook[testItem] + value string +} + +type testList = List[testItem, *testItem] + +func collectValues(l *testList) []string { + var out []string + for it := l.Front(); it != nil; it = it.Next() { + out = append(out, it.value) + } + return out +} + +func TestTLZeroValue(t *testing.T) { + var l testList + require.True(t, l.Empty()) + require.Nil(t, l.Front()) + require.Nil(t, l.Back()) +} + +func TestTLPushFrontBackAndRemove(t *testing.T) { + var l testList + a := &testItem{value: "a"} + b := &testItem{value: "b"} + c := &testItem{value: "c"} + + l.PushFront(b) + l.PushFront(a) + l.PushBack(c) + + require.Equal(t, []string{"a", "b", "c"}, collectValues(&l)) + require.Same(t, a, l.Front()) + require.Same(t, c, l.Back()) + require.Same(t, b, a.Next()) + require.Same(t, b, c.Prev()) + + l.Remove(b) + require.Equal(t, []string{"a", "c"}, collectValues(&l)) + require.Nil(t, b.Next()) + require.Nil(t, b.Prev()) + + l.Remove(a) + l.Remove(c) + require.True(t, l.Empty()) + require.Nil(t, l.Front()) + require.Nil(t, l.Back()) +} + +func TestTLInsertAndMove(t *testing.T) { + var l testList + a := &testItem{value: "a"} + b := &testItem{value: "b"} + c := &testItem{value: "c"} + d := &testItem{value: "d"} + + l.PushBack(a) + l.PushBack(c) + l.InsertAfter(b, a) + l.InsertBefore(d, a) + + require.Equal(t, []string{"d", "a", "b", "c"}, collectValues(&l)) + + l.MoveToFront(c) + require.Equal(t, []string{"c", "d", "a", "b"}, collectValues(&l)) + + l.MoveToBack(d) + require.Equal(t, []string{"c", "a", "b", "d"}, collectValues(&l)) + require.Same(t, c, l.Front()) + require.Same(t, d, l.Back()) +} From 35315401c322dd3107d92766f4c73ec97ea633b2 Mon Sep 17 00:00:00 2001 From: Paul Wells Date: Wed, 11 Mar 2026 21:39:01 -0700 Subject: [PATCH 2/2] Create chilled-rivers-dream.md --- .changeset/chilled-rivers-dream.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/chilled-rivers-dream.md diff --git a/.changeset/chilled-rivers-dream.md b/.changeset/chilled-rivers-dream.md new file mode 100644 index 000000000..cacd73249 --- /dev/null +++ b/.changeset/chilled-rivers-dream.md @@ -0,0 +1,5 @@ +--- +"@livekit/protocol": patch +--- + +add generic list type