skip to content

Why is a push-based stream source called a protocol between three roles rather than a producer invoking a consumer callback?

level: middleimportance: should knowfreq 46%

answer

  1. count the parties, not the calls
  2. a callback has no ending
  3. failure needs its own channel
  4. a handle per attachment, given first
  5. named rules make stages interchangeable

basics

~20 s

Because three parties are named and bound by rules: a source, a consumer, and a per-subscription handle created when they attach. A bare callback has no ending, no separate failure channel, no identity for one run, and no way to answer back.

solid answer

~50 s

A callback is one function the producer invokes with a value; everything else is convention. The stream contract names **three roles** — the source that produces, the consumer that receives, and the **handle** created for that one attachment — and then constrains what they may do. The consumer is given its handle before any value can arrive, so the run has an identity and a back-channel from the start. The source promises a grammar (values, then at most one terminal) and a delivery discipline (one signal at a time). Failure is its own signal kind rather than an exception thrown at whoever happened to call. Because those rules are named rather than assumed, a stage written by one author can sit between a source and a consumer written by others — which a callback, whose behaviour is whatever its caller chose, cannot support.

code

pseudocode · 8 lines
pseudocode
// callback shape: one function, no ending, no failure channel
source.onEach(function(value): handle(value))

// protocol shape: three roles and a fixed grammar
source.attach(consumer)
consumer.attached(handle)      // this run only, before any value
consumer.value(v)              // any number, one at a time
consumer.completed()           // or consumer.failure(reason) - at most one

go deeper

for a junior

Be able to name the three parties — source, consumer, and the handle created when they attach — and one thing a plain callback cannot express, such as the ending.

for a middle

Walk through what each part of the protocol removes: no ending means the receiver can never release state, no failure channel means errors travel as data or as exceptions at the wrong caller.

for a senior

Argue the composability case: independently written stages connect only because the grammar and delivery discipline are agreed, and say honestly when a callback is still the right tool.

for a principal

Weigh the cost of the ceremony against what it buys, and be clear that it earns its keep at authorship boundaries rather than inside a single module.

## The three roles Describe the contract as parties and obligations rather than as functions and it becomes much easier to reason about. - **The source** produces. It obeys the grammar, delivers serially, and reports failure as a signal. - **The consumer** receives. It accepts value signals and at most one terminal signal, and may release its per-run state the moment the terminal arrives. - **The handle** is created when a consumer attaches, belongs to that single attachment, and is given to the consumer *before* any value can arrive. It is the back-channel: the consumer's way to say something about this particular run rather than about the source in general. That third role is the one a callback has no equivalent for, and it is why the arrangement is a protocol. What the consumer says through the handle — how much it is ready for, and that it wants no more — is the subject of neighbouring material; the point here is that a named, per-attachment channel exists at all, and that it is established first. ## What a bare value callback lacks | what the receiver gets | plain value callback | three-role protocol | |---|---|---| | an ending | none; the calls simply stop | at most one terminal signal, and silence after it | | a failure path | an exception thrown at whoever called, or failure smuggled into a value | its own signal kind carrying a reason | | identity for one run | none; the function is just a function | a handle bound to this attachment | | delivery discipline | whatever the caller happens to do | one signal at a time, with an ordering edge | | a way to answer back | none | the handle, established before the first value | The first row is the one that bites in practice. With a callback, *nothing stops* means either the source finished, or it died, or it is merely quiet — the receiver cannot tell, so it can never safely release what it accumulated. Give the ending a name and the receiver's job becomes finite. ## Why the handle is per-subscription rather than per-source Two consumers attaching to the same source are two runs. Each gets its own handle, and what one says through its handle concerns only its own run. A shared handle would make one consumer able to speak for another, which is how you get a consumer that stops receiving because an unrelated one lost interest. Per-attachment identity is also what makes the grammar well defined: at most one terminal *per run*, not one per source, so a source may end one consumer's run while another's continues. The ordering matters too. The handle is given first, before any value, so the consumer is never in the position of receiving data it has no way to respond about. ## What the protocol buys that convention cannot 1. **Composability.** A stage that transforms or filters can sit between any conforming source and any conforming consumer, because it knows exactly what it will receive and exactly what it owes onward. With callbacks, every pairing is a negotiation. 2. **Interoperability across authors.** Independently written sources, stages and consumers connect without shared code, because the grammar, the delivery discipline and the failure channel are agreed rather than per-library habit. 3. **Checkability.** A protocol with named rules can be tested for conformance mechanically. Convention cannot be. 4. **Finite obligations.** Every participant knows when it is done: at the terminal, release everything. ## The claim to reject The dismissive version — *a stream is a callback with extra ceremony* — mistakes ceremony for the point. The extra parts are not decoration: each one removes a question the receiver would otherwise be unable to answer. Is it over? Did it fail, or finish? Which run is this? Can two of these arrive at once? May I let go of my buffer now? A callback answers none of them; the protocol answers all of them the same way for every source that claims to be one. The ceremony *is* the contract, and the contract is what makes the pieces interchangeable. One honest qualifier: for a single producer and a single receiver inside one module, a callback really is enough, and adding a protocol buys nothing. The protocol earns its keep exactly when the pieces cross an authorship boundary — which is the situation it was designed for.

  • What does a named contract buy when the source and the consumer come from different authors?
    Interoperability without shared code. Any conforming source can be handed to any conforming consumer, and a stage written once works between them, because the grammar, the serial delivery promise and the failure channel are agreed in advance rather than discovered per library.
  • Does the handle reach the consumer before or after the first value?
    Before. The consumer is given its handle when it attaches, so the run has a back-channel from the start. A source that delivers a value first leaves the consumer holding data it has no way to answer about, and the ordering rule exists precisely to rule that out.

saying these in an interview costs you the question

  • Calls a stream a callback with extra ceremony and stops there
  • Thinks a callback already carries an ending signal
  • Expects one handle shared by every consumer of a source
  • Claims the contract exists to make delivery faster
  • Assumes failure arrives as a value the receiver must inspect
  • Says the handle is created after the first value arrives