refactor(daemon): define render coordinator contract
Inert S1 contract: coordinator state, options, deadline bounds, resize metadata ownership, lifecycle/sync/preview signatures, and the session installation hook. No scheduling, composition, or producer migration — behavior arrives in the GREEN slice.
- commit
- 65114bebf59dfcbed120e187bc2de380987b31c4
- author
- brice · · 2026-07-11 14:41 UTC
- parent
- 1c718e0
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +167 -0
A internal/usecase/daemon/render_coordinator.go +165 -0
| @@ -0,0 +1,165 @@ | |||
| 1 | + | package daemon | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "sync" | |
| 5 | + | "time" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/vev/internal/domain" | |
| 8 | + | "github.com/bnema/vev/internal/ports" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // S1 render coordinator contract. One coordinator serves one attached | |
| 12 | + | // session: producers mutate authoritative state and publish an invalidation; | |
| 13 | + | // only the coordinator decides when the transitional composition target runs. | |
| 14 | + | // Producers never compose or send. This file currently carries the inert | |
| 15 | + | // contract — state, options, and signatures exist so the behavioral | |
| 16 | + | // specification compiles — while wake scheduling, ACK gating, synchronized | |
| 17 | + | // output handling, and producer migration land in the GREEN slice. | |
| 18 | + | ||
| 19 | + | // Coordinator deadline bounds. Urgent transitions (input echo, overlay state | |
| 20 | + | // changes) must become visible within urgentRenderDeadline; bulk output | |
| 21 | + | // coalesces inside the adaptive [minOutputRenderDeadline, | |
| 22 | + | // maxOutputRenderDeadline] window. | |
| 23 | + | const ( | |
| 24 | + | urgentRenderDeadline = 2 * time.Millisecond | |
| 25 | + | minOutputRenderDeadline = 8 * time.Millisecond | |
| 26 | + | maxOutputRenderDeadline = 16 * time.Millisecond | |
| 27 | + | ) | |
| 28 | + | ||
| 29 | + | // invalidationClass ranks a producer transition's latency demand. | |
| 30 | + | type invalidationClass uint8 | |
| 31 | + | ||
| 32 | + | const ( | |
| 33 | + | // invalidateOutput is bulk PTY/screen churn served by the adaptive window. | |
| 34 | + | invalidateOutput invalidationClass = iota | |
| 35 | + | // invalidateUrgent is an interactive transition served within 2ms. | |
| 36 | + | invalidateUrgent | |
| 37 | + | ) | |
| 38 | + | ||
| 39 | + | // renderInvalidation is one producer-published state transition. | |
| 40 | + | type renderInvalidation struct { | |
| 41 | + | class invalidationClass | |
| 42 | + | // reset requests a sticky FullRedraw: once requested it survives | |
| 43 | + | // coalescing and ACK deferral until a wake actually delivers it. | |
| 44 | + | reset bool | |
| 45 | + | // producer identifies the publishing call-site for provenance. | |
| 46 | + | producer string | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | // renderWake is one coalesced composition request delivered to the | |
| 50 | + | // transitional composition target (replaced by the S2 pipeline). | |
| 51 | + | type renderWake struct { | |
| 52 | + | reset bool | |
| 53 | + | urgent bool | |
| 54 | + | coalesced int | |
| 55 | + | // watchdog marks a flush forced by the synchronized-output watchdog. | |
| 56 | + | watchdog bool | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | // resizeRequestMetadata is the coordinator-owned latest requested resize | |
| 60 | + | // state: S3's transaction entry point reads it through resizeSnapshot. It is | |
| 61 | + | // metadata ownership only — S1/S2 dispatch stays with the retained PR #71 | |
| 62 | + | // attachment timer path. | |
| 63 | + | type resizeRequestMetadata struct { | |
| 64 | + | size domain.Size | |
| 65 | + | source *attachedClient | |
| 66 | + | epoch uint64 | |
| 67 | + | } | |
| 68 | + | ||
| 69 | + | // renderCoordinatorOptions wires one coordinator instance. | |
| 70 | + | type renderCoordinatorOptions struct { | |
| 71 | + | clock ports.Clock | |
| 72 | + | // wake is the transitional composition target. | |
| 73 | + | wake func(renderWake) | |
| 74 | + | // ackReady reports whether the attachment may compose another output | |
| 75 | + | // state (the outputStateStream window has capacity). | |
| 76 | + | ackReady func() bool | |
| 77 | + | // syncActive reports whether a synchronized-output batch is open. | |
| 78 | + | syncActive func() bool | |
| 79 | + | // onInvalidate observes every published invalidation (test-visible hook). | |
| 80 | + | onInvalidate func(renderInvalidation) | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | // renderCoordinator fans in producer invalidations for one attached session. | |
| 84 | + | type renderCoordinator struct { | |
| 85 | + | mu sync.Mutex | |
| 86 | + | opts renderCoordinatorOptions | |
| 87 | + | ||
| 88 | + | // Coalesced pending state: latest wins, reset stays sticky, and at most | |
| 89 | + | // one deadline wake may be armed at a time (cap-1). | |
| 90 | + | pending bool | |
| 91 | + | pendingReset bool | |
| 92 | + | pendingUrgent bool | |
| 93 | + | coalesced int | |
| 94 | + | ||
| 95 | + | // previewWake receives the same coalesced wakes while a preview | |
| 96 | + | // subscription is installed. | |
| 97 | + | previewWake func(renderWake) | |
| 98 | + | ||
| 99 | + | // attachment is the currently bound client identity; callbacks from any | |
| 100 | + | // other identity are stale and must not mutate coordinator state. | |
| 101 | + | attachment *attachedClient | |
| 102 | + | torndown bool | |
| 103 | + | ||
| 104 | + | resize resizeRequestMetadata | |
| 105 | + | } | |
| 106 | + | ||
| 107 | + | // newRenderCoordinator constructs a coordinator bound to opts. It starts no | |
| 108 | + | // goroutine; deadline scheduling is armed lazily by invalidate. | |
| 109 | + | func newRenderCoordinator(opts renderCoordinatorOptions) *renderCoordinator { | |
| 110 | + | return &renderCoordinator{opts: opts} | |
| 111 | + | } | |
| 112 | + | ||
| 113 | + | // invalidate publishes one producer state transition. Consecutive | |
| 114 | + | // invalidations coalesce under a single armed deadline; the latest state and | |
| 115 | + | // the stickiest reset win. | |
| 116 | + | func (c *renderCoordinator) invalidate(renderInvalidation) {} | |
| 117 | + | ||
| 118 | + | // notifyAck reports that the client acknowledged an output state, releasing | |
| 119 | + | // at most one deferred wake. | |
| 120 | + | func (c *renderCoordinator) notifyAck() {} | |
| 121 | + | ||
| 122 | + | // noteSyncBegin records that a synchronized-output batch opened for gen and | |
| 123 | + | // arms the completion watchdog. | |
| 124 | + | func (c *renderCoordinator) noteSyncBegin(uint64) {} | |
| 125 | + | ||
| 126 | + | // noteSyncEnd records that the batch for gen completed, flushing pending | |
| 127 | + | // state in one wake. | |
| 128 | + | func (c *renderCoordinator) noteSyncEnd(uint64) {} | |
| 129 | + | ||
| 130 | + | // subscribePreview installs fn as the preview observer for coalesced wakes. | |
| 131 | + | func (c *renderCoordinator) subscribePreview(func(renderWake)) {} | |
| 132 | + | ||
| 133 | + | // teardownPreview removes the preview subscription. | |
| 134 | + | func (c *renderCoordinator) teardownPreview() {} | |
| 135 | + | ||
| 136 | + | // attach binds ac as the coordinator's current attachment identity. | |
| 137 | + | func (c *renderCoordinator) attach(*attachedClient) {} | |
| 138 | + | ||
| 139 | + | // noteDetach invalidates pending wakes and stale callbacks for a detaching | |
| 140 | + | // attachment. | |
| 141 | + | func (c *renderCoordinator) noteDetach(*attachedClient) {} | |
| 142 | + | ||
| 143 | + | // noteReplace hands the coordinator from old to replacement; callbacks | |
| 144 | + | // captured by old become stale. | |
| 145 | + | func (c *renderCoordinator) noteReplace(old, replacement *attachedClient) {} | |
| 146 | + | ||
| 147 | + | // notePark invalidates pending wakes when the attachment parks for resume. | |
| 148 | + | func (c *renderCoordinator) notePark(*attachedClient) {} | |
| 149 | + | ||
| 150 | + | // noteSessionTeardown terminally invalidates the coordinator. | |
| 151 | + | func (c *renderCoordinator) noteSessionTeardown() {} | |
| 152 | + | ||
| 153 | + | // recordResizeRequest records the latest requested geometry and source before | |
| 154 | + | // the request delegates to the retained PR #71 attachment path. It returns | |
| 155 | + | // the strictly monotonically increased epoch, or 0 when the source is stale. | |
| 156 | + | func (c *renderCoordinator) recordResizeRequest(domain.Size, *attachedClient) uint64 { return 0 } | |
| 157 | + | ||
| 158 | + | // resizeSnapshot returns a locked copy of the latest resize metadata. | |
| 159 | + | func (c *renderCoordinator) resizeSnapshot() resizeRequestMetadata { return resizeRequestMetadata{} } | |
| 160 | + | ||
| 161 | + | // renderCoordinator returns the coordinator installed for this session. | |
| 162 | + | func (s *session) renderCoordinator() *renderCoordinator { return s.coordinator.Load() } | |
| 163 | + | ||
| 164 | + | // installRenderCoordinator publishes rc as the session's coordinator. | |
| 165 | + | func (s *session) installRenderCoordinator(rc *renderCoordinator) { s.coordinator.Store(rc) } | |
| @@ -0,0 +1,165 @@ | |||||
| 1 | + | package daemon | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "sync" | |||
| 5 | + | "time" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/vev/internal/domain" | |||
| 8 | + | "github.com/bnema/vev/internal/ports" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | // S1 render coordinator contract. One coordinator serves one attached | |||
| 12 | + | // session: producers mutate authoritative state and publish an invalidation; | |||
| 13 | + | // only the coordinator decides when the transitional composition target runs. | |||
| 14 | + | // Producers never compose or send. This file currently carries the inert | |||
| 15 | + | // contract — state, options, and signatures exist so the behavioral | |||
| 16 | + | // specification compiles — while wake scheduling, ACK gating, synchronized | |||
| 17 | + | // output handling, and producer migration land in the GREEN slice. | |||
| 18 | + | ||||
| 19 | + | // Coordinator deadline bounds. Urgent transitions (input echo, overlay state | |||
| 20 | + | // changes) must become visible within urgentRenderDeadline; bulk output | |||
| 21 | + | // coalesces inside the adaptive [minOutputRenderDeadline, | |||
| 22 | + | // maxOutputRenderDeadline] window. | |||
| 23 | + | const ( | |||
| 24 | + | urgentRenderDeadline = 2 * time.Millisecond | |||
| 25 | + | minOutputRenderDeadline = 8 * time.Millisecond | |||
| 26 | + | maxOutputRenderDeadline = 16 * time.Millisecond | |||
| 27 | + | ) | |||
| 28 | + | ||||
| 29 | + | // invalidationClass ranks a producer transition's latency demand. | |||
| 30 | + | type invalidationClass uint8 | |||
| 31 | + | ||||
| 32 | + | const ( | |||
| 33 | + | // invalidateOutput is bulk PTY/screen churn served by the adaptive window. | |||
| 34 | + | invalidateOutput invalidationClass = iota | |||
| 35 | + | // invalidateUrgent is an interactive transition served within 2ms. | |||
| 36 | + | invalidateUrgent | |||
| 37 | + | ) | |||
| 38 | + | ||||
| 39 | + | // renderInvalidation is one producer-published state transition. | |||
| 40 | + | type renderInvalidation struct { | |||
| 41 | + | class invalidationClass | |||
| 42 | + | // reset requests a sticky FullRedraw: once requested it survives | |||
| 43 | + | // coalescing and ACK deferral until a wake actually delivers it. | |||
| 44 | + | reset bool | |||
| 45 | + | // producer identifies the publishing call-site for provenance. | |||
| 46 | + | producer string | |||
| 47 | + | } | |||
| 48 | + | ||||
| 49 | + | // renderWake is one coalesced composition request delivered to the | |||
| 50 | + | // transitional composition target (replaced by the S2 pipeline). | |||
| 51 | + | type renderWake struct { | |||
| 52 | + | reset bool | |||
| 53 | + | urgent bool | |||
| 54 | + | coalesced int | |||
| 55 | + | // watchdog marks a flush forced by the synchronized-output watchdog. | |||
| 56 | + | watchdog bool | |||
| 57 | + | } | |||
| 58 | + | ||||
| 59 | + | // resizeRequestMetadata is the coordinator-owned latest requested resize | |||
| 60 | + | // state: S3's transaction entry point reads it through resizeSnapshot. It is | |||
| 61 | + | // metadata ownership only — S1/S2 dispatch stays with the retained PR #71 | |||
| 62 | + | // attachment timer path. | |||
| 63 | + | type resizeRequestMetadata struct { | |||
| 64 | + | size domain.Size | |||
| 65 | + | source *attachedClient | |||
| 66 | + | epoch uint64 | |||
| 67 | + | } | |||
| 68 | + | ||||
| 69 | + | // renderCoordinatorOptions wires one coordinator instance. | |||
| 70 | + | type renderCoordinatorOptions struct { | |||
| 71 | + | clock ports.Clock | |||
| 72 | + | // wake is the transitional composition target. | |||
| 73 | + | wake func(renderWake) | |||
| 74 | + | // ackReady reports whether the attachment may compose another output | |||
| 75 | + | // state (the outputStateStream window has capacity). | |||
| 76 | + | ackReady func() bool | |||
| 77 | + | // syncActive reports whether a synchronized-output batch is open. | |||
| 78 | + | syncActive func() bool | |||
| 79 | + | // onInvalidate observes every published invalidation (test-visible hook). | |||
| 80 | + | onInvalidate func(renderInvalidation) | |||
| 81 | + | } | |||
| 82 | + | ||||
| 83 | + | // renderCoordinator fans in producer invalidations for one attached session. | |||
| 84 | + | type renderCoordinator struct { | |||
| 85 | + | mu sync.Mutex | |||
| 86 | + | opts renderCoordinatorOptions | |||
| 87 | + | ||||
| 88 | + | // Coalesced pending state: latest wins, reset stays sticky, and at most | |||
| 89 | + | // one deadline wake may be armed at a time (cap-1). | |||
| 90 | + | pending bool | |||
| 91 | + | pendingReset bool | |||
| 92 | + | pendingUrgent bool | |||
| 93 | + | coalesced int | |||
| 94 | + | ||||
| 95 | + | // previewWake receives the same coalesced wakes while a preview | |||
| 96 | + | // subscription is installed. | |||
| 97 | + | previewWake func(renderWake) | |||
| 98 | + | ||||
| 99 | + | // attachment is the currently bound client identity; callbacks from any | |||
| 100 | + | // other identity are stale and must not mutate coordinator state. | |||
| 101 | + | attachment *attachedClient | |||
| 102 | + | torndown bool | |||
| 103 | + | ||||
| 104 | + | resize resizeRequestMetadata | |||
| 105 | + | } | |||
| 106 | + | ||||
| 107 | + | // newRenderCoordinator constructs a coordinator bound to opts. It starts no | |||
| 108 | + | // goroutine; deadline scheduling is armed lazily by invalidate. | |||
| 109 | + | func newRenderCoordinator(opts renderCoordinatorOptions) *renderCoordinator { | |||
| 110 | + | return &renderCoordinator{opts: opts} | |||
| 111 | + | } | |||
| 112 | + | ||||
| 113 | + | // invalidate publishes one producer state transition. Consecutive | |||
| 114 | + | // invalidations coalesce under a single armed deadline; the latest state and | |||
| 115 | + | // the stickiest reset win. | |||
| 116 | + | func (c *renderCoordinator) invalidate(renderInvalidation) {} | |||
| 117 | + | ||||
| 118 | + | // notifyAck reports that the client acknowledged an output state, releasing | |||
| 119 | + | // at most one deferred wake. | |||
| 120 | + | func (c *renderCoordinator) notifyAck() {} | |||
| 121 | + | ||||
| 122 | + | // noteSyncBegin records that a synchronized-output batch opened for gen and | |||
| 123 | + | // arms the completion watchdog. | |||
| 124 | + | func (c *renderCoordinator) noteSyncBegin(uint64) {} | |||
| 125 | + | ||||
| 126 | + | // noteSyncEnd records that the batch for gen completed, flushing pending | |||
| 127 | + | // state in one wake. | |||
| 128 | + | func (c *renderCoordinator) noteSyncEnd(uint64) {} | |||
| 129 | + | ||||
| 130 | + | // subscribePreview installs fn as the preview observer for coalesced wakes. | |||
| 131 | + | func (c *renderCoordinator) subscribePreview(func(renderWake)) {} | |||
| 132 | + | ||||
| 133 | + | // teardownPreview removes the preview subscription. | |||
| 134 | + | func (c *renderCoordinator) teardownPreview() {} | |||
| 135 | + | ||||
| 136 | + | // attach binds ac as the coordinator's current attachment identity. | |||
| 137 | + | func (c *renderCoordinator) attach(*attachedClient) {} | |||
| 138 | + | ||||
| 139 | + | // noteDetach invalidates pending wakes and stale callbacks for a detaching | |||
| 140 | + | // attachment. | |||
| 141 | + | func (c *renderCoordinator) noteDetach(*attachedClient) {} | |||
| 142 | + | ||||
| 143 | + | // noteReplace hands the coordinator from old to replacement; callbacks | |||
| 144 | + | // captured by old become stale. | |||
| 145 | + | func (c *renderCoordinator) noteReplace(old, replacement *attachedClient) {} | |||
| 146 | + | ||||
| 147 | + | // notePark invalidates pending wakes when the attachment parks for resume. | |||
| 148 | + | func (c *renderCoordinator) notePark(*attachedClient) {} | |||
| 149 | + | ||||
| 150 | + | // noteSessionTeardown terminally invalidates the coordinator. | |||
| 151 | + | func (c *renderCoordinator) noteSessionTeardown() {} | |||
| 152 | + | ||||
| 153 | + | // recordResizeRequest records the latest requested geometry and source before | |||
| 154 | + | // the request delegates to the retained PR #71 attachment path. It returns | |||
| 155 | + | // the strictly monotonically increased epoch, or 0 when the source is stale. | |||
| 156 | + | func (c *renderCoordinator) recordResizeRequest(domain.Size, *attachedClient) uint64 { return 0 } | |||
| 157 | + | ||||
| 158 | + | // resizeSnapshot returns a locked copy of the latest resize metadata. | |||
| 159 | + | func (c *renderCoordinator) resizeSnapshot() resizeRequestMetadata { return resizeRequestMetadata{} } | |||
| 160 | + | ||||
| 161 | + | // renderCoordinator returns the coordinator installed for this session. | |||
| 162 | + | func (s *session) renderCoordinator() *renderCoordinator { return s.coordinator.Load() } | |||
| 163 | + | ||||
| 164 | + | // installRenderCoordinator publishes rc as the session's coordinator. | |||
| 165 | + | func (s *session) installRenderCoordinator(rc *renderCoordinator) { s.coordinator.Store(rc) } | |||
M internal/usecase/daemon/session.go +2 -0
| @@ -67,6 +67,8 @@ type session struct { | |||
| 67 | 67 | mruAt atomic.Uint64 | |
| 68 | 68 | snapDirty atomic.Bool | |
| 69 | 69 | snapEligible atomic.Bool | |
| 70 | + | // coordinator fans in this session's producer render invalidations. | |
| 71 | + | coordinator atomic.Pointer[renderCoordinator] | |
| 70 | 72 | // clipFiles records clipboard-image-transfer temp file paths (see | |
| 71 | 73 | // clipboard.go) written for this session, removed best-effort in | |
| 72 | 74 | // killSession. | |
| @@ -67,6 +67,8 @@ type session struct { | |||||
| 67 | mruAt atomic.Uint64 | 67 | mruAt atomic.Uint64 | ||
| 68 | snapDirty atomic.Bool | 68 | snapDirty atomic.Bool | ||
| 69 | snapEligible atomic.Bool | 69 | snapEligible atomic.Bool | ||
| 70 | + | // coordinator fans in this session's producer render invalidations. | |||
| 71 | + | coordinator atomic.Pointer[renderCoordinator] | |||
| 70 | // clipFiles records clipboard-image-transfer temp file paths (see | 72 | // clipFiles records clipboard-image-transfer temp file paths (see | ||
| 71 | // clipboard.go) written for this session, removed best-effort in | 73 | // clipboard.go) written for this session, removed best-effort in | ||
| 72 | // killSession. | 74 | // killSession. | ||