Files

5.7 KiB

Subjects Guide

Subjects are both Observable and Observer — they can receive values (via Send, Error, Complete) and be subscribed to. Subjects are natively hot: subscribers share a single execution, and late subscribers only see future emissions (unless replay is configured).

When to Use Subjects vs Cold Observables

Use case Approach
Data pipeline from a known source (slice, channel, HTTP) Cold observable (default)
Event bus where producers and consumers are decoupled Subject
Multiple consumers need the same WebSocket/ticker stream Cold observable + Share() or ShareReplay()
Imperatively push values from non-reactive code Subject
Bridge between callback API and reactive pipeline Subject (receive callbacks, emit to pipeline)

Subject Types

PublishSubject

Standard multicast. Subscribers only see values emitted after they subscribe.

subject := ro.NewPublishSubject[string]()

// Subscriber 1
subject.Subscribe(ro.OnNext(func(s string) {
    fmt.Println("sub1:", s)
}))

subject.Send("hello")  // sub1 sees this

// Subscriber 2 (late)
subject.Subscribe(ro.OnNext(func(s string) {
    fmt.Println("sub2:", s)
}))

subject.Send("world")  // both see this
subject.Complete()

Use when: broadcasting events where late subscribers don't need history — UI events, log streams, notifications.

BehaviorSubject

Replays the last emitted value (or the initial value) to every new subscriber immediately on subscription.

subject := ro.NewBehaviorSubject[int](0) // initial value = 0

// Subscriber 1 immediately receives 0
subject.Subscribe(ro.OnNext(func(v int) {
    fmt.Println("sub1:", v) // 0, then 42
}))

subject.Send(42)

// Subscriber 2 immediately receives 42 (latest value)
subject.Subscribe(ro.OnNext(func(v int) {
    fmt.Println("sub2:", v) // 42
}))

Use when: subscribers need the current state — config values, connection status, latest price.

ReplaySubject

Buffers the last N values and replays them to every new subscriber.

subject := ro.NewReplaySubject[string](3) // buffer size = 3

subject.Send("a")
subject.Send("b")
subject.Send("c")
subject.Send("d") // "a" evicted from buffer

// Late subscriber receives "b", "c", "d" (last 3)
subject.Subscribe(ro.OnNext(func(s string) {
    fmt.Println(s)
}))

Use when: late subscribers need recent history — chat messages, recent logs, last N stock ticks.

AsyncSubject

Emits only the last value and only when the subject completes. If the subject errors, no value is emitted.

subject := ro.NewAsyncSubject[int]()

subject.Subscribe(ro.NewObserver(
    func(v int) { fmt.Println(v) },    // receives 3 only
    func(err error) { },
    func() { fmt.Println("done") },
))

subject.Send(1)
subject.Send(2)
subject.Send(3)
subject.Complete() // triggers emission of 3, then "done"

Use when: only the final result matters — computation result, last response in a batch.

UnicastSubject

Allows exactly one subscriber. Buffers values internally until that subscriber connects.

subject := ro.NewUnicastSubject[int](100) // buffer size

subject.Send(1) // buffered
subject.Send(2) // buffered

// Single subscriber receives buffered + future values
subject.Subscribe(ro.OnNext(func(v int) {
    fmt.Println(v) // 1, 2, then future values
}))
// Second subscribe would panic or error

Use when: single consumer with buffering — job queues, request pipelines where exactly one handler processes events.

Cold to Hot Conversion

When you have a cold observable (e.g. an HTTP request) but need multiple subscribers to share it:

Share

// Each subscriber to `cold` would trigger a separate HTTP request
cold := httpPlugin.Get[Data](url)

// Share: single execution, multiple subscribers
hot := ro.Pipe1(cold, ro.Share[Data]())

hot.Subscribe(uiObserver)       // shares one HTTP call
hot.Subscribe(metricsObserver)  // same data, no extra request

Share uses reference counting: the source subscribes when the first subscriber arrives and unsubscribes when the last one leaves.

ShareReplay

// Late subscribers get the last N values + future values
hot := ro.Pipe1(cold, ro.ShareReplay[Data](1))

Connectable Observable

For precise control over when the shared subscription starts:

connectable := ro.Connectable[Data](cold)

// Set up subscribers first
connectable.Subscribe(observer1)
connectable.Subscribe(observer2)

// Start the shared execution explicitly
sub, err := connectable.Connect(ctx)

Subject Decision Table

Subject Replay Subscribers Use case
PublishSubject None Many Event bus, notifications
BehaviorSubject Last 1 (+ initial) Many Current state, config
ReplaySubject Last N Many Recent history, chat
AsyncSubject Last 1 (on complete) Many Final computation result
UnicastSubject Buffered (pre-subscribe) Exactly 1 Single-consumer queue

Common Subject Mistakes

Mistake Why Fix
Calling Send() after Complete() Values are silently dropped — the subject is terminal Track lifecycle, don't reuse completed subjects
Using PublishSubject when late subscribers need history Late subscribers miss all prior events Use BehaviorSubject (last 1) or ReplaySubject (last N)
Using ReplaySubject with unbounded buffer Memory grows without limit Set an explicit bufferSize
Multiple subscribers on UnicastSubject Panics or undefined behavior Use PublishSubject for multicast, UnicastSubject for single consumer
Not calling Complete() on subjects Subscribers wait forever, goroutine leak Always Complete() or Error() when the source is done