Scaffold loopback daemon client split

This commit is contained in:
2026-05-27 13:19:56 +01:00
parent e63bdad5e1
commit 9aecc8b7a2
8 changed files with 436 additions and 36 deletions

View File

@@ -46,6 +46,13 @@ type Session struct {
listenersMu sync.Mutex
listeners atomic.Pointer[[]ChildEventListener]
// clientListeners is the network-client subscriber path. These
// listeners must be non-blocking and copy PTY chunks before enqueueing;
// daemon-internal observers (timers, debug capture, waiters) stay on
// listeners above so backpressure policy is isolated to clients.
clientListenersMu sync.Mutex
clientListeners atomic.Pointer[[]ChildEventListener]
// persistStore records top-level command entries to a per-project
// JSON file so they can be re-spawned after patterm restarts.
// Optional; nil means "no persistence" (used by unit tests).
@@ -118,6 +125,16 @@ func (s *Session) Subscribe(l ChildEventListener) {
s.listeners.Store(&next)
}
func (s *Session) SubscribeClient(l ChildEventListener) {
s.clientListenersMu.Lock()
defer s.clientListenersMu.Unlock()
prev := s.clientListenersSnapshot()
next := make([]ChildEventListener, 0, len(prev)+1)
next = append(next, prev...)
next = append(next, l)
s.clientListeners.Store(&next)
}
// Unsubscribe removes a previously-registered listener. Safe to call
// with a listener that wasn't registered (no-op).
func (s *Session) Unsubscribe(l ChildEventListener) {
@@ -146,16 +163,30 @@ func (s *Session) listenersSnapshot() []ChildEventListener {
return *p
}
func (s *Session) clientListenersSnapshot() []ChildEventListener {
p := s.clientListeners.Load()
if p == nil {
return nil
}
return *p
}
func (s *Session) emitSpawn(c *Child) {
for _, l := range s.listenersSnapshot() {
l.OnChildSpawned(c)
}
for _, l := range s.clientListenersSnapshot() {
l.OnChildSpawned(c)
}
}
func (s *Session) emitExit(c *Child) {
for _, l := range s.listenersSnapshot() {
l.OnChildExited(c)
}
for _, l := range s.clientListenersSnapshot() {
l.OnChildExited(c)
}
}
// emitPTYOut dispatches a fresh PTY chunk to every listener. Listeners
@@ -165,18 +196,27 @@ func (s *Session) emitPTYOut(id string, chunk []byte) {
for _, l := range s.listenersSnapshot() {
l.OnPTYOut(id, chunk)
}
for _, l := range s.clientListenersSnapshot() {
l.OnPTYOut(id, chunk)
}
}
func (s *Session) emitStateChanged(id string, state IdleState) {
for _, l := range s.listenersSnapshot() {
l.OnChildStateChanged(id, state)
}
for _, l := range s.clientListenersSnapshot() {
l.OnChildStateChanged(id, state)
}
}
func (s *Session) emitClosed(id string) {
for _, l := range s.listenersSnapshot() {
l.OnChildClosed(id)
}
for _, l := range s.clientListenersSnapshot() {
l.OnChildClosed(id)
}
}
func (s *Session) ChildEnv() []string {