Understanding the publish Operator

The publish operator serves as a way to inject a Subject into the observable pipeline. By doing so, it enables multiple subscribers to share a single underlying subscription.

In practice, publish acts as a convenient shorthand for the multicast operator, which means grasping how multicast functions is essential before diving into publish. You can find a detailed explanation of multicast behavior here.

Below is an illustration of how multicast allows subscription 1 and subscription 2 to share the same subscription to the source obs stream:

const log = (index) => (v) => console.log(`subscription ${index}\t: ` + v);

const obs = interval(200).pipe(
   take(3),
   tap({ complete() { console.log('underlying stream completed') }})
);

const shared = obs.pipe(
   publish()
);

shared.subscribe(log(1));
shared.subscribe(log(2));

shared.connect();

Just like multicast, the publish operator returns a ConnectableObservable that requires an explicit connection to the source stream to begin emitting.

Here is what the preceding code produces:

subscription 1	: 0
subscription 2	: 0
subscription 1	: 1
subscription 2	: 1
subscription 1	: 2
subscription 2	: 2
underlying stream completed

Observe that although both subscriptions receive all the values, the source obs stream is subscribed to only once, with that single subscription being shared across subsctiption 1 and subsctipion 2. Consequently, only a single COMPLETE notification appears.

For a clearer demonstration, we can build a custom Observable that logs each emitted value:

function producer(observer) {
   let counter = 0;

   const id = setInterval(() => {
       console.log('producer\t\t: ' + counter);
       observer.next(counter);
       counter++;
   }, 200);

   return () => clearInterval(id);
}

const obs = new Observable(producer).pipe(
   take(3),
   tap({ complete() { console.log('underlying stream completed') }})
);

const shared = obs.pipe(
   publish()
);

shared.subscribe(log(1));
shared.subscribe(log(2));

shared.connect();

The corresponding output is shown below:

producer		: 0
subscription 1	: 0
subscription 2	: 0
producer		: 1
subscription 1	: 1
subscription 2	: 1
producer		: 2
subscription 1	: 2
subscription 2	: 2
underlying stream completed

Notice that 3 values appear in the console: 2 from the subscription handlers and only one from the producer function. This confirms that the producer runs once, and the resulting stream is distributed to all subscribers.

As noted earlier, publish serves as a shorthand for the multicast operator with a Subject. Swapping one for the other yields identical results, as seen here:

const shared = obs.pipe(
   multicast(new Subject())
);

Like multicast, publish simply inserts a Subject into the observable chain. The sharing mechanism itself is handled by the Subject, not by the publish or multicast operators, which exist purely to streamline working with Subject instances.

The key difference lies in flexibility: with multicast, you can supply your own Subject instance. With publish, however, the Subject is instantiated internally and remains inaccessible to the outside world.

Furthermore, mulicast offers additional power through its factory function parameter. This factory can generate fresh Subject instances on demand, potentially multiple times, whereas publish uses a single internally created instance. More details on this behavior are available in the multicast reference.

Consider this example where multicast receives a factory function that produces a Subject. The outcome differs because the underlying subscription gets reactivated:

const obs = new Observable(producer).pipe(
   take(3),
   tap({ complete() { console.log('underlying stream completed') }})
);

const shared = obs.pipe(
   multicast(() => new Subject())
) as ConnectableObservable<any>;

shared.subscribe(log(1));
shared.subscribe(log(2));

shared.connect();

setTimeout(() => { shared.subscribe(log(3)); shared.connect() }, 1000);

Running this code produces the following result:

producer		: 0
subscription 1	: 0
subscription 2	: 0
producer		: 1
subscription 1	: 1
subscription 2	: 1
producer		: 2
subscription 1	: 2
subscription 2	: 2
underlying stream completed
producer		: 0
subscription 3	: 0
producer		: 1
subscription 3	: 1
producer		: 2
subscription 3	: 2
underlying stream completed

Pay attention to how the 3rd subscription receives all 3 values, and the underlying stream emits two separate COMPLETE notifications.

producer		: 0
subscription 3	: 0
producer		: 1
subscription 3	: 1
producer		: 2
subscription 3	: 2
underlying stream completed

This behavior is a direct consequence of using multicast with a factory function.

Finally, publish has two notable variants that swap the standard Subject for either a ReplaySubject or a BehaviorSubject. These are the publishReplay and publishBehavior operators.