1package daemon
2
3import (
4 "github.com/bnema/vev/internal/protocol"
5 "github.com/bnema/vev/internal/usecase/palette"
6)
7
8// This file owns the serving-daemon side of navigation inventory: demand
9// emission, publication admission, and imported-row selection. All overlay
10// fields below are guarded by paletteMu alongside the palette model; no
11// lock is held across sends. Helpers never lock internally: callers hold
12// paletteMu for state transitions and release it before any sendControl.
13
14// inventoryDemandEnabled reports whether this attachment may receive
15// inventory demand. The serving daemon emits demand only for the advertised
16// capability; the client independently checks its concrete local source.
17func inventoryDemandEnabled(ac *attachedClient) bool {
18 return ac != nil && ac.navigationCapabilities&protocol.NavigationCapabilityInventory != 0
19}
20
21// openPaletteInventory starts one palette interaction namespace. Admitted
22// keys and retirements never cross interactions.
23func openPaletteInventory(rt *overlayRuntime) uint64 {
24 rt.paletteInventoryOpen = true
25 rt.paletteInventoryInteraction++
26 rt.paletteInventoryPublication = 0
27 rt.paletteInventoryGroups = nil
28 rt.paletteInventorySelected = nil
29 rt.paletteInventoryDemandSent = false
30 return rt.paletteInventoryInteraction
31}
32
33// closePaletteInventory ends the interaction and drops relayed rows.
34// Imported rows vanish with the palette; a reopen starts a new namespace.
35// The pending selection survives the close so a late resolve failure still
36// correlates to the interaction that chose it; the next open clears it.
37func closePaletteInventory(rt *overlayRuntime) {
38 rt.paletteInventoryOpen = false
39 rt.paletteInventoryGroups = nil
40 rt.paletteInventoryDemandSent = false
41}
42
43// takePaletteInventoryClose snapshots a pending close-demand: the overlay
44// must be open for the interaction. Callers send Demand{open:false} after
45// releasing paletteMu, then close through clearPaletteLocked.
46func takePaletteInventoryClose(rt *overlayRuntime) (uint64, bool) {
47 if !rt.paletteInventoryOpen {
48 return 0, false
49 }
50 return rt.paletteInventoryInteraction, true
51}
52
53// sendPaletteInventoryDemand emits an open or close demand on the guarded
54// serving connection. It locks paletteMu only for the emission check, never
55// across the send. A nil effect skips the send while the overlay state still
56// transitions; those paths coincide with attachment teardown or replacement
57// where the client-side relay is cancelled independently. Stale close
58// demands (superseded by a reopen) skip so generations stay ordered.
59func (d *Daemon) sendPaletteInventoryDemand(ac *attachedClient, effect *attachmentEffect, open bool, interaction uint64) {
60 if !inventoryDemandEnabled(ac) || effect == nil || interaction == 0 {
61 return
62 }
63 ac.overlays.paletteMu.Lock()
64 if open {
65 if !ac.overlays.paletteInventoryOpen || ac.overlays.paletteInventoryInteraction != interaction || ac.overlays.paletteInventoryDemandSent {
66 ac.overlays.paletteMu.Unlock()
67 return
68 }
69 ac.overlays.paletteInventoryDemandSent = true
70 } else if ac.overlays.paletteInventoryInteraction != interaction {
71 ac.overlays.paletteMu.Unlock()
72 return
73 }
74 ac.overlays.paletteMu.Unlock()
75 demand := protocol.NavigationInventoryDemand{InteractionGeneration: interaction, Open: open}
76 if protocol.ValidateNavigationInventoryDemand(demand) != nil {
77 return
78 }
79 _ = effect.sendControl(demand)
80}
81
82// admitPaletteInventoryPublication validates and stores one client
83// publication. Older or duplicate publication generations discard: the
84// daemon never steps its display backward. Stale interactions (palette
85// closed or reopened since) drop silently.
86func admitPaletteInventoryPublication(rt *overlayRuntime, message protocol.NavigationInventoryPublication) bool {
87 if protocol.ValidateNavigationInventoryPublication(message) != nil {
88 return false
89 }
90 if !rt.paletteInventoryOpen || message.InteractionGeneration != rt.paletteInventoryInteraction {
91 return false
92 }
93 if message.PublicationGeneration <= rt.paletteInventoryPublication {
94 return false
95 }
96 rt.paletteInventoryPublication = message.PublicationGeneration
97 rt.paletteInventoryGroups = append([]protocol.NavigationInventorySourceGroup(nil), message.Groups...)
98 return true
99}
100
101// copyPaletteInventoryGroups snapshots the admitted relayed groups for
102// projection. It copies under paletteMu and releases it before any capture:
103// callers must never nest it inside registry locks. A closed palette
104// yields nil: imported rows vanish with the overlay.
105func copyPaletteInventoryGroups(ac *attachedClient) []protocol.NavigationInventorySourceGroup {
106 if ac == nil || ac.overlays == nil {
107 return nil
108 }
109 ac.overlays.paletteMu.Lock()
110 defer ac.overlays.paletteMu.Unlock()
111 if !ac.overlays.paletteInventoryOpen {
112 return nil
113 }
114 return append([]protocol.NavigationInventorySourceGroup(nil), ac.overlays.paletteInventoryGroups...)
115}
116
117// importedPaletteResults projects admitted sanitized groups as palette rows
118// with explicit origin qualification. Homonyms across sources stay distinct
119// rows; selection resolves through opaque keys, never labels.
120func importedPaletteResults(groups []protocol.NavigationInventorySourceGroup) []palette.Result {
121 var results []palette.Result
122 for _, group := range groups {
123 if group.Status != protocol.NavigationInventorySourceOK {
124 continue
125 }
126 for _, entry := range group.Entries {
127 origin := entry.DisplayOrigin
128 if origin == "" {
129 origin = group.SourceKey
130 }
131 results = append(results, palette.NewImportedSessionResult(group.SourceKey, entry.EntryKey, entry.Name, origin, entry.State, entry.Reason))
132 }
133 }
134 return results
135}
136
137// appendImportedResults projects admitted relayed rows after native
138// results. Enter starts native-only (fresh namespace); refreshes pick up
139// streamed publications. Stable opaque keys preserve cursor and query
140// across ReplaceResults.
141func appendImportedResults(results []palette.Result, ac *attachedClient) []palette.Result {
142 groups := copyPaletteInventoryGroups(ac)
143 if ac == nil || ac.overlays == nil {
144 return results
145 }
146 localKeys := make(map[string]bool)
147 for _, group := range groups {
148 if group.SourceKey == protocol.NavigationInventoryLocalSourceKey && group.Status == protocol.NavigationInventorySourceOK {
149 for _, entry := range group.Entries {
150 localKeys[entry.EntryKey] = true
151 }
152 }
153 }
154 ac.overlays.paletteMu.Lock()
155 snapshot := ac.overlays.paletteRouteSnapshot
156 ac.overlays.paletteMu.Unlock()
157 filtered := results[:0]
158 for _, result := range results {
159 if action, ok := result.RouteNavigationAction(); ok {
160 entry, found := routeEntryForRef(snapshot, protocol.RouteRef{Key: action.Key, Generation: action.Generation})
161 if found && entry.Kind == protocol.RouteKindLocal && localKeys[navigationInventoryEntryKey(entry.Target.LifecycleID, entry.Target.SessionName)] {
162 continue
163 }
164 }
165 filtered = append(filtered, result)
166 }
167 return append(filtered, importedPaletteResults(groups)...)
168}
169
170// inventoryFailureNotice maps a bounded relay failure code to palette
171// feedback through existing conventions. Native commands stay available:
172// only the contextual feedback line changes.
173func inventoryFailureNotice(code protocol.NavigationInventoryFailureCode) string {
174 switch code {
175 case protocol.NavigationInventoryStaleIdentity, protocol.NavigationInventoryInvalidTarget:
176 return "that destination is no longer available"
177 case protocol.NavigationInventorySourceGone:
178 return "local inventory source unavailable"
179 case protocol.NavigationInventoryIncompatible:
180 return "incompatible session version"
181 case protocol.NavigationInventoryNavigationFailed:
182 return "couldn't switch sessions"
183 case protocol.NavigationInventoryRestoreFailed:
184 return "couldn't restore the original session"
185 default:
186 return "inventory selection unavailable"
187 }
188}
189
190// freezePaletteInventorySelection records the chosen imported row before its
191// Selection crosses the wire. Closing after selection must not cancel the
192// client's pending navigation operation.
193func freezePaletteInventorySelection(rt *overlayRuntime, selection protocol.NavigationInventorySelection) {
194 rt.paletteInventorySelected = &selection
195}