Phases: attach lifecycle + atomic publish batch

Every stream store exposes its attach machine as a read-only phase store (initialized -> attaching -> attached -> detached | failed) plus explicit attach() / detach() controls. And on the server, ctx.batch(fn) turns publishes into an all-or-nothing batch: a throw after an await retracts publishes issued before it.

Reading as Breezy Turnip (85a0077d)

Subscription lifecycle

Feed hidden - the subscription is . Attach to render it.

This page attached for you on load. Press Detach, then Attach - every hop the machine takes lands in the log above, including the attaching that usually resolves too fast to catch on the badge. attach() holds an internal retain: the stream stays subscribed with no UI subscriber and reattaches itself across outages; failed is the hop you get when the server refuses the attach. detach() is "done" - it releases the retain and tears the subscription down immediately, skipping the resume-grace window a normal component unmount gets (the short hold that lets a quick remount resume the same subscription - see it in the counter-resume demo).

Atomic publish batch

Both buttons run the same handler: ctx.batch(async () => { publish first; await; maybe throw; publish second }). The failing variant throws BETWEEN the two publishes, after a real await boundary - and neither entry appears, because the collector buffers publishes across awaits precisely so a later throw can retract them. Bare ctx.publish would already have flushed the first one at the await.