diff --git a/cover.html b/cover.html
new file mode 100644
index 0000000..106e984
--- /dev/null
+++ b/cover.html
@@ -0,0 +1,248 @@
+
+
+
+
+
+
+
package bus
+
+import (
+ "fmt"
+ "reflect"
+ "sync"
+
+ "github.com/gopatchy/metadata"
+)
+
+type Bus struct {
+ mu sync.Mutex
+ keyViews map[string]map[uintptr]chan<- any
+ typeViews map[string]map[uintptr]chan<- any
+}
+
+func NewBus() *Bus {
+ return &Bus{
+ keyViews: map[string]map[uintptr]chan<- any{},
+ typeViews: map[string]map[uintptr]chan<- any{},
+ }
+}
+
+func (b *Bus) Announce(t string, obj any) {
+ key := getObjKey(t, obj)
+
+ b.mu.Lock()
+ defer b.mu.Unlock()
+
+ announce(obj, b.keyViews[key])
+ announce(obj, b.typeViews[t])
+}
+
+func (b *Bus) Delete(t string, id string) {
+ key := getKey(t, id)
+
+ b.mu.Lock()
+ defer b.mu.Unlock()
+
+ for _, c := range b.keyViews[key] {
+ close(c)
+ }
+
+ delete(b.keyViews, key)
+
+ announce(id, b.typeViews[t])
+}
+
+func (b *Bus) SubscribeKey(t, id string, initial any) <-chan any {
+ key := getKey(t, id)
+
+ b.mu.Lock()
+ defer b.mu.Unlock()
+
+ ret := make(chan any, 100)
+
+ ret <- initial
+
+ if _, has := b.keyViews[key]; !has {
+ b.keyViews[key] = map[uintptr]chan<- any{}
+ }
+
+ b.keyViews[key][chanID(ret)] = ret
+
+ return ret
+}
+
+func (b *Bus) SubscribeType(t string, initial any) <-chan any {
+ b.mu.Lock()
+ defer b.mu.Unlock()
+
+ ret := make(chan any, 100)
+
+ ret <- initial
+
+ if _, has := b.typeViews[t]; !has {
+ b.typeViews[t] = map[uintptr]chan<- any{}
+ }
+
+ b.typeViews[t][chanID(ret)] = ret
+
+ return ret
+}
+
+func (b *Bus) UnsubscribeKey(t, id string, c <-chan any) {
+ key := getKey(t, id)
+
+ b.mu.Lock()
+ defer b.mu.Unlock()
+
+ if cw, has := b.keyViews[key][chanID(c)]; has {
+ close(cw)
+ delete(b.keyViews[key], chanID(c))
+ }
+
+ if len(b.keyViews[key]) == 0 {
+ delete(b.keyViews, key)
+ }
+}
+
+func (b *Bus) UnsubscribeType(t string, c <-chan any) {
+ b.mu.Lock()
+ defer b.mu.Unlock()
+
+ if cw, has := b.typeViews[t][chanID(c)]; has {
+ close(cw)
+ delete(b.typeViews[t], chanID(c))
+ }
+
+ if len(b.typeViews[t]) == 0 {
+ delete(b.typeViews, t)
+ }
+}
+
+func getObjKey(t string, obj any) string {
+ return getKey(t, metadata.GetMetadata(obj).ID)
+}
+
+func getKey(t string, id string) string {
+ return fmt.Sprintf("%s:%s", t, id)
+}
+
+func announce(obj any, chans map[uintptr]chan<- any) {
+ for id, c := range chans {
+ select {
+ case c <- obj:
+ default:
+ close(c)
+ delete(chans, id)
+ }
+ }
+}
+
+func chanID(c <-chan any) uintptr {
+ return reflect.ValueOf(c).Pointer()
+}
+
+
+
+
+
+
diff --git a/cover.out b/cover.out
new file mode 100644
index 0000000..887ba16
--- /dev/null
+++ b/cover.out
@@ -0,0 +1,27 @@
+mode: atomic
+github.com/gopatchy/bus/bus.go:17.20,22.2 1 2
+github.com/gopatchy/bus/bus.go:24.43,32.2 5 5
+github.com/gopatchy/bus/bus.go:34.43,40.36 4 1
+github.com/gopatchy/bus/bus.go:40.36,42.3 1 1
+github.com/gopatchy/bus/bus.go:44.2,46.30 2 1
+github.com/gopatchy/bus/bus.go:49.66,59.37 6 5
+github.com/gopatchy/bus/bus.go:59.37,61.3 1 4
+github.com/gopatchy/bus/bus.go:63.2,65.12 2 5
+github.com/gopatchy/bus/bus.go:68.63,76.36 5 3
+github.com/gopatchy/bus/bus.go:76.36,78.3 1 3
+github.com/gopatchy/bus/bus.go:80.2,82.12 2 3
+github.com/gopatchy/bus/bus.go:85.58,91.48 4 5
+github.com/gopatchy/bus/bus.go:91.48,94.3 2 4
+github.com/gopatchy/bus/bus.go:96.2,96.31 1 5
+github.com/gopatchy/bus/bus.go:96.31,98.3 1 4
+github.com/gopatchy/bus/bus.go:101.55,105.47 3 3
+github.com/gopatchy/bus/bus.go:105.47,108.3 2 3
+github.com/gopatchy/bus/bus.go:110.2,110.30 1 3
+github.com/gopatchy/bus/bus.go:110.30,112.3 1 3
+github.com/gopatchy/bus/bus.go:115.42,117.2 1 5
+github.com/gopatchy/bus/bus.go:119.41,121.2 1 16
+github.com/gopatchy/bus/bus.go:123.54,124.27 1 11
+github.com/gopatchy/bus/bus.go:124.27,125.10 1 10
+github.com/gopatchy/bus/bus.go:126.17,126.17 0 10
+github.com/gopatchy/bus/bus.go:127.11,129.21 2 0
+github.com/gopatchy/bus/bus.go:134.35,136.2 1 23