@@ -48,9 +48,13 @@ Executor legend used below:
4848
4949## Publish
5050
51- A publisher's local forwarder ** is** the publisher forwarder — the same object serves as the
52- on-thread data-plane forwarder and as the registry's publisher entry (no second forwarder is
53- constructed on ` relayExec_ ` , unlike the ` RelayExec ` -mode branch in ` publishWithSession ` ).
51+ A publisher's local forwarder ** is** the publisher forwarder — a single forwarder serves the
52+ whole track, living on the publisher's executor rather than on ` relayExec_ ` . Ownership lives in
53+ ` tlForwarders_ ` on the publisher's executor, ** not** in the registry: once registration
54+ completes, ` registerPublishOnRelayExec ` calls ` registry_.clearForwarder(ftn) ` , so the registry
55+ entry keeps its subscription/namespace bookkeeping but drops its forwarder ref. Code that needs
56+ the forwarder resolves it by ` FullTrackName ` from ` tlForwarders_ ` on the owning executor, rather
57+ than holding a registry ref.
5458
5559### Setup
5660
@@ -65,7 +69,8 @@ constructed on `relayExec_`, unlike the `RelayExec`-mode branch in `publishWithS
6569[Relay] └─▶ registerPublishOnRelayExec()
6670[Relay] ├─▶ publishWithSession() # registry + namespace tree
6771[Relay] │ └─▶ addSubscriberAndPublish() # attach existing SUBSCRIBE_NAMESPACE subs (see Publish fan-out)
68- [Relay] └─▶ relayChainFilter->setDownstream(topNFilter)
72+ [Relay] ├─▶ relayChainFilter->setDownstream(topNFilter)
73+ [Relay] └─▶ registry_.clearForwarder(ftn) # drop registry ref; tlForwarders_ owns the fwd
6974[Pub] return {consumer, replyTask} # immediate
7075```
7176
@@ -133,11 +138,13 @@ upstream work, and nests a single sortie to `[Pub]` to wire the channel sub.
133138[Sub] ├─▶ localFwd->addSubscriber() # so `forward` is right pre-hop
134139[Sub] └─▶ ⇢⇢▶ [Relay] attachNewLocalForwarderOnRelayExec()
135140[Relay] ├─▶ joinOrPrepareUpstreamSubscription() # registry: first vs subsequent
136- [Relay] ├─▶ buildLocalToPublisherCallbacks()
137141[Relay] └─▶ ⇢⇢▶ [Pub] single sortie:
142+ [Pub] ├─ if SUBSEQUENT: resolve live publisherFwd from tlForwarders_
143+ [Pub] ├─▶ buildLocalToPublisherCallbacks()
138144[Pub] ├─ if FIRST subscriber:
139145[Pub] │ └─▶ installPublisherForwarderCallbackChain() # chain + tlForwarders_ slot
140146[Pub] ├─▶ installChannelSubscriber(localFwd ↔ publisherFwd)
147+ [Pub] ├─ if SUBSEQUENT: read seed largest/extensions
141148[Pub] └─ if FIRST subscriber:
142149[Pub] ├─▶ addChannelSubscriber(relayChain, passive)
143150[Pub] └─▶ subscribeUpstreamAndApplyOk()
@@ -147,7 +154,7 @@ upstream work, and nests a single sortie to `[Pub]` to wire the channel sub.
147154[Sub] ◀── back on subscriberExec (tail)
148155[Sub] ├─ sawOnEmpty? ─▶ teardownLocalForwarderOnFailure()
149156[Sub] ├─ error? ─▶ publishDone + remove
150- [Sub] ├─▶ apply upstreamOk extensions / largest
157+ [Sub] ├─▶ apply largest/extensions ( upstreamOk if FIRST, else seeded)
151158[Sub] ├─▶ replayPendingFowarderEvents() # drain buffered events
152159[Sub] └─▶ return sub
153160```
@@ -158,12 +165,16 @@ publisher-forwarder control chain and claims the `tlForwarders_` slot via
158165` installPublisherForwarderCallbackChain ` — the ** same** wiring the publish path uses, so a
159166subscribe-initiated publisher forwarder is symmetric with a publish-initiated one. Subsequent
160167subscribers on the same thread hit the ` attachSubscriber ` fast path (now also for
161- subscribe-initiated tracks); on other threads they take only the ` installChannelSubscriber ` half
162- of the sortie.
168+ subscribe-initiated tracks); on other threads they take the non-first half of the sortie:
169+ resolve the live publisher forwarder from ` tlForwarders_ ` on the publisher exec, install the
170+ channel sub, and seed ` largest ` /extensions from it.
163171
164172The forwarder is created on ` relayExec_ ` (in ` joinOrPrepareUpstreamSubscription ` ) with a ** null**
165173callback, then gets its real callback + tl slot installed on ` [Pub] ` in the first-subscriber
166- sortie — ` tlForwarders_.get() ` must run on the forwarder's own exec. It uses ` removeOnEmpty=true `
174+ sortie — ` tlForwarders_.get() ` must run on the forwarder's own exec. Once that sortie claims the
175+ tl slot, the relay-exec continuation calls ` registry_.clearForwarder(ftn) ` , transferring ownership
176+ to ` tlForwarders_ ` and leaving the registry with only bookkeeping — the same as the publish path.
177+ It uses ` removeOnEmpty=true `
167178(subscribe-initiated tracks drop on empty, unlike publish): ` LocalForwarderCallback ` vacates the
168179tl slot on ` [Pub] ` when the last subscriber leaves, while ` onEmptyImpl ` unsubscribes upstream and
169180removes the registry entry on ` [Relay] ` .
0 commit comments