refactor(client): group picker session state
- commit
- b96d645fd0fa30761801a262272b335687dfe7bb
- author
- brice · · 2026-10-08 05:58 UTC
- committer
- brice · · 2026-10-08 08:12 UTC
- parent
- 4992fe8
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
6 files · +57 -45
M internal/usecase/client/preview_manager_test.go +5 -5
| @@ -342,15 +342,15 @@ func TestSupervisorRefreshPreviewWiresTerminalGeometryToPreviewManager(t *testin | |||
| 342 | 342 | h.service.EXPECT().SubscribePreview(expected).Return(sub, nil).Once() | |
| 343 | 343 | sub.EXPECT().Close().Once() | |
| 344 | 344 | ||
| 345 | - | supervisor := &Supervisor{cfg: SupervisorConfig{Picker: h.controller, Terminal: &pickerRecordingTerminal{}}, preview: previewManager{clock: h.clock}} | |
| 345 | + | supervisor := &Supervisor{cfg: SupervisorConfig{Picker: h.controller, Terminal: &pickerRecordingTerminal{}}, picker: pickerSession{preview: previewManager{clock: h.clock}}} | |
| 346 | 346 | supervisor.refreshPreview(h.service) | |
| 347 | 347 | h.clock.awaitTimer(t).fire() | |
| 348 | 348 | select { | |
| 349 | - | case <-supervisor.preview.changed(): | |
| 349 | + | case <-supervisor.picker.preview.changed(): | |
| 350 | 350 | case <-time.After(5 * time.Second): | |
| 351 | 351 | t.Fatal("the supervisor preview did not wake") | |
| 352 | 352 | } | |
| 353 | - | supervisor.preview.publish(h.controller) | |
| 354 | - | require.Equal(t, expected, supervisor.preview.request) | |
| 355 | - | supervisor.preview.close(h.controller) | |
| 353 | + | supervisor.picker.preview.publish(h.controller) | |
| 354 | + | require.Equal(t, expected, supervisor.picker.preview.request) | |
| 355 | + | supervisor.picker.preview.close(h.controller) | |
| 356 | 356 | } | |
| @@ -342,15 +342,15 @@ func TestSupervisorRefreshPreviewWiresTerminalGeometryToPreviewManager(t *testin | |||||
| 342 | h.service.EXPECT().SubscribePreview(expected).Return(sub, nil).Once() | 342 | h.service.EXPECT().SubscribePreview(expected).Return(sub, nil).Once() | ||
| 343 | sub.EXPECT().Close().Once() | 343 | sub.EXPECT().Close().Once() | ||
| 344 | 344 | ||||
| 345 | - | supervisor := &Supervisor{cfg: SupervisorConfig{Picker: h.controller, Terminal: &pickerRecordingTerminal{}}, preview: previewManager{clock: h.clock}} | 345 | + | supervisor := &Supervisor{cfg: SupervisorConfig{Picker: h.controller, Terminal: &pickerRecordingTerminal{}}, picker: pickerSession{preview: previewManager{clock: h.clock}}} |
| 346 | supervisor.refreshPreview(h.service) | 346 | supervisor.refreshPreview(h.service) | ||
| 347 | h.clock.awaitTimer(t).fire() | 347 | h.clock.awaitTimer(t).fire() | ||
| 348 | select { | 348 | select { | ||
| 349 | - | case <-supervisor.preview.changed(): | 349 | + | case <-supervisor.picker.preview.changed(): |
| 350 | case <-time.After(5 * time.Second): | 350 | case <-time.After(5 * time.Second): | ||
| 351 | t.Fatal("the supervisor preview did not wake") | 351 | t.Fatal("the supervisor preview did not wake") | ||
| 352 | } | 352 | } | ||
| 353 | - | supervisor.preview.publish(h.controller) | 353 | + | supervisor.picker.preview.publish(h.controller) |
| 354 | - | require.Equal(t, expected, supervisor.preview.request) | 354 | + | require.Equal(t, expected, supervisor.picker.preview.request) |
| 355 | - | supervisor.preview.close(h.controller) | 355 | + | supervisor.picker.preview.close(h.controller) |
| 356 | } | 356 | } | ||
M internal/usecase/client/supervisor.go +15 -18
| @@ -434,10 +434,9 @@ type Supervisor struct { | |||
| 434 | 434 | cfg SupervisorConfig | |
| 435 | 435 | logger *slog.Logger | |
| 436 | 436 | ||
| 437 | - | // pendingPickerKey retains a commit observed while connecting. It is | |
| 438 | - | // revalidated against the adopted service just like a ready-phase commit. | |
| 439 | - | pendingPickerKey string | |
| 440 | - | preview previewManager | |
| 437 | + | // picker groups the picker's per-run state: the pending commit key, the | |
| 438 | + | // preview subscription, and the off-goroutine kill runner. | |
| 439 | + | picker pickerSession | |
| 441 | 440 | ||
| 442 | 441 | mu sync.Mutex | |
| 443 | 442 | state State | |
| @@ -471,8 +470,6 @@ type Supervisor struct { | |||
| 471 | 470 | // nav is the run goroutine's navigation bookkeeping: the ready-phase | |
| 472 | 471 | // subscription, pending swap and in-place choices, and the route ledger. | |
| 473 | 472 | nav navigationState | |
| 474 | - | // kills runs the picker's `x` operations off the run goroutine. | |
| 475 | - | kills pickerKills | |
| 476 | 473 | } | |
| 477 | 474 | ||
| 478 | 475 | // NewSupervisor validates the required dependencies and returns a supervisor | |
| @@ -520,7 +517,7 @@ func NewSupervisor(cfg SupervisorConfig) (*Supervisor, error) { | |||
| 520 | 517 | state: State{Presentation: PresentPicker, Connectivity: ConnectivityDisconnected}, | |
| 521 | 518 | clientID: newClientID(), | |
| 522 | 519 | navigation: cfg.InitialNavigation, | |
| 523 | - | preview: previewManager{clock: cfg.Clock}, | |
| 520 | + | picker: pickerSession{preview: previewManager{clock: cfg.Clock}}, | |
| 524 | 521 | } | |
| 525 | 522 | // The host is the supervisor's existing foreground grant. It owns no raw | |
| 526 | 523 | // mode and starts no reader: the supervisor keeps its one terminal input | |
| @@ -592,7 +589,7 @@ func (s *Supervisor) Run(ctx context.Context) (retErr error) { | |||
| 592 | 589 | ) | |
| 593 | 590 | retire := func() { | |
| 594 | 591 | s.retirePickerKill() | |
| 595 | - | s.preview.close(s.cfg.Picker) | |
| 592 | + | s.picker.preview.close(s.cfg.Picker) | |
| 596 | 593 | s.nav.clearReady() | |
| 597 | 594 | if sub != nil { | |
| 598 | 595 | sub.Close() | |
| @@ -898,8 +895,8 @@ type readyOutcome struct { | |||
| 898 | 895 | // the supervisor can admit exactly the row the user committed. It opens no | |
| 899 | 896 | // stream and starts no terminal work itself. | |
| 900 | 897 | func (s *Supervisor) awaitReady(ctx context.Context, input *terminalInputLifetime, service ports.BrokerNavigator, sub ports.BrokerSubscription) readyOutcome { | |
| 901 | - | if key := s.pendingPickerKey; key != "" { | |
| 902 | - | s.pendingPickerKey = "" | |
| 898 | + | if key := s.picker.pendingPickerKey; key != "" { | |
| 899 | + | s.picker.pendingPickerKey = "" | |
| 903 | 900 | return readyOutcome{commitKey: key} | |
| 904 | 901 | } | |
| 905 | 902 | var changed <-chan struct{} | |
| @@ -922,11 +919,11 @@ func (s *Supervisor) awaitReady(ctx context.Context, input *terminalInputLifetim | |||
| 922 | 919 | s.cfg.Picker.ApplySnapshot(service.Snapshot()) | |
| 923 | 920 | s.refreshPreview(service) | |
| 924 | 921 | s.renderCurrent() | |
| 925 | - | case <-s.preview.changed(): | |
| 926 | - | if s.preview.publish(s.cfg.Picker) { | |
| 922 | + | case <-s.picker.preview.changed(): | |
| 923 | + | if s.picker.preview.publish(s.cfg.Picker) { | |
| 927 | 924 | s.renderCurrent() | |
| 928 | 925 | } | |
| 929 | - | case outcome := <-s.kills.results(): | |
| 926 | + | case outcome := <-s.picker.kills.results(): | |
| 930 | 927 | s.finishPickerKill(service, outcome) | |
| 931 | 928 | case <-ops: | |
| 932 | 929 | op, key := s.cfg.Picker.TakeOp() | |
| @@ -944,7 +941,7 @@ func (s *Supervisor) awaitReady(ctx context.Context, input *terminalInputLifetim | |||
| 944 | 941 | if op.exit { | |
| 945 | 942 | // Ctrl+C is the explicit exit. With no attachment to | |
| 946 | 943 | // return to, it is the only close that ends the process. | |
| 947 | - | s.preview.close(s.cfg.Picker) | |
| 944 | + | s.picker.preview.close(s.cfg.Picker) | |
| 948 | 945 | return readyOutcome{terminated: true} | |
| 949 | 946 | } | |
| 950 | 947 | // Escape and q cancel; with no attachment there is nothing to | |
| @@ -954,7 +951,7 @@ func (s *Supervisor) awaitReady(ctx context.Context, input *terminalInputLifetim | |||
| 954 | 951 | continue | |
| 955 | 952 | } | |
| 956 | 953 | if op.commit && key != "" { | |
| 957 | - | s.preview.close(s.cfg.Picker) | |
| 954 | + | s.picker.preview.close(s.cfg.Picker) | |
| 958 | 955 | return readyOutcome{commitKey: key} | |
| 959 | 956 | } | |
| 960 | 957 | case <-ctx.Done(): | |
| @@ -974,10 +971,10 @@ func (s *Supervisor) refreshPreview(service ports.BrokerNavigator) { | |||
| 974 | 971 | } | |
| 975 | 972 | geometry, err := s.cfg.Terminal.Geometry() | |
| 976 | 973 | if err != nil || !geometry.Valid() { | |
| 977 | - | s.preview.close(s.cfg.Picker) | |
| 974 | + | s.picker.preview.close(s.cfg.Picker) | |
| 978 | 975 | return | |
| 979 | 976 | } | |
| 980 | - | s.preview.refresh(service, s.cfg.Picker, geometry.Size) | |
| 977 | + | s.picker.preview.refresh(service, s.cfg.Picker, geometry.Size) | |
| 981 | 978 | } | |
| 982 | 979 | ||
| 983 | 980 | // pickerOps is nil when no picker owns presentation decisions. | |
| @@ -994,7 +991,7 @@ func (s *Supervisor) pickerOps() <-chan struct{} { | |||
| 994 | 991 | func (s *Supervisor) takePickerClose() bool { | |
| 995 | 992 | op, key := s.cfg.Picker.TakeOp() | |
| 996 | 993 | if op.commit && key != "" { | |
| 997 | - | s.pendingPickerKey = key | |
| 994 | + | s.picker.pendingPickerKey = key | |
| 998 | 995 | } | |
| 999 | 996 | if op.close && !op.exit { | |
| 1000 | 997 | s.notifyPicker(pickerExitHint) | |
| @@ -434,10 +434,9 @@ type Supervisor struct { | |||||
| 434 | cfg SupervisorConfig | 434 | cfg SupervisorConfig | ||
| 435 | logger *slog.Logger | 435 | logger *slog.Logger | ||
| 436 | 436 | ||||
| 437 | - | // pendingPickerKey retains a commit observed while connecting. It is | 437 | + | // picker groups the picker's per-run state: the pending commit key, the |
| 438 | - | // revalidated against the adopted service just like a ready-phase commit. | 438 | + | // preview subscription, and the off-goroutine kill runner. |
| 439 | - | pendingPickerKey string | 439 | + | picker pickerSession |
| 440 | - | preview previewManager | |||
| 441 | 440 | ||||
| 442 | mu sync.Mutex | 441 | mu sync.Mutex | ||
| 443 | state State | 442 | state State | ||
| @@ -471,8 +470,6 @@ type Supervisor struct { | |||||
| 471 | // nav is the run goroutine's navigation bookkeeping: the ready-phase | 470 | // nav is the run goroutine's navigation bookkeeping: the ready-phase | ||
| 472 | // subscription, pending swap and in-place choices, and the route ledger. | 471 | // subscription, pending swap and in-place choices, and the route ledger. | ||
| 473 | nav navigationState | 472 | nav navigationState | ||
| 474 | - | // kills runs the picker's `x` operations off the run goroutine. | |||
| 475 | - | kills pickerKills | |||
| 476 | } | 473 | } | ||
| 477 | 474 | ||||
| 478 | // NewSupervisor validates the required dependencies and returns a supervisor | 475 | // NewSupervisor validates the required dependencies and returns a supervisor | ||
| @@ -520,7 +517,7 @@ func NewSupervisor(cfg SupervisorConfig) (*Supervisor, error) { | |||||
| 520 | state: State{Presentation: PresentPicker, Connectivity: ConnectivityDisconnected}, | 517 | state: State{Presentation: PresentPicker, Connectivity: ConnectivityDisconnected}, | ||
| 521 | clientID: newClientID(), | 518 | clientID: newClientID(), | ||
| 522 | navigation: cfg.InitialNavigation, | 519 | navigation: cfg.InitialNavigation, | ||
| 523 | - | preview: previewManager{clock: cfg.Clock}, | 520 | + | picker: pickerSession{preview: previewManager{clock: cfg.Clock}}, |
| 524 | } | 521 | } | ||
| 525 | // The host is the supervisor's existing foreground grant. It owns no raw | 522 | // The host is the supervisor's existing foreground grant. It owns no raw | ||
| 526 | // mode and starts no reader: the supervisor keeps its one terminal input | 523 | // mode and starts no reader: the supervisor keeps its one terminal input | ||
| @@ -592,7 +589,7 @@ func (s *Supervisor) Run(ctx context.Context) (retErr error) { | |||||
| 592 | ) | 589 | ) | ||
| 593 | retire := func() { | 590 | retire := func() { | ||
| 594 | s.retirePickerKill() | 591 | s.retirePickerKill() | ||
| 595 | - | s.preview.close(s.cfg.Picker) | 592 | + | s.picker.preview.close(s.cfg.Picker) |
| 596 | s.nav.clearReady() | 593 | s.nav.clearReady() | ||
| 597 | if sub != nil { | 594 | if sub != nil { | ||
| 598 | sub.Close() | 595 | sub.Close() | ||
| @@ -898,8 +895,8 @@ type readyOutcome struct { | |||||
| 898 | // the supervisor can admit exactly the row the user committed. It opens no | 895 | // the supervisor can admit exactly the row the user committed. It opens no | ||
| 899 | // stream and starts no terminal work itself. | 896 | // stream and starts no terminal work itself. | ||
| 900 | func (s *Supervisor) awaitReady(ctx context.Context, input *terminalInputLifetime, service ports.BrokerNavigator, sub ports.BrokerSubscription) readyOutcome { | 897 | func (s *Supervisor) awaitReady(ctx context.Context, input *terminalInputLifetime, service ports.BrokerNavigator, sub ports.BrokerSubscription) readyOutcome { | ||
| 901 | - | if key := s.pendingPickerKey; key != "" { | 898 | + | if key := s.picker.pendingPickerKey; key != "" { |
| 902 | - | s.pendingPickerKey = "" | 899 | + | s.picker.pendingPickerKey = "" |
| 903 | return readyOutcome{commitKey: key} | 900 | return readyOutcome{commitKey: key} | ||
| 904 | } | 901 | } | ||
| 905 | var changed <-chan struct{} | 902 | var changed <-chan struct{} | ||
| @@ -922,11 +919,11 @@ func (s *Supervisor) awaitReady(ctx context.Context, input *terminalInputLifetim | |||||
| 922 | s.cfg.Picker.ApplySnapshot(service.Snapshot()) | 919 | s.cfg.Picker.ApplySnapshot(service.Snapshot()) | ||
| 923 | s.refreshPreview(service) | 920 | s.refreshPreview(service) | ||
| 924 | s.renderCurrent() | 921 | s.renderCurrent() | ||
| 925 | - | case <-s.preview.changed(): | 922 | + | case <-s.picker.preview.changed(): |
| 926 | - | if s.preview.publish(s.cfg.Picker) { | 923 | + | if s.picker.preview.publish(s.cfg.Picker) { |
| 927 | s.renderCurrent() | 924 | s.renderCurrent() | ||
| 928 | } | 925 | } | ||
| 929 | - | case outcome := <-s.kills.results(): | 926 | + | case outcome := <-s.picker.kills.results(): |
| 930 | s.finishPickerKill(service, outcome) | 927 | s.finishPickerKill(service, outcome) | ||
| 931 | case <-ops: | 928 | case <-ops: | ||
| 932 | op, key := s.cfg.Picker.TakeOp() | 929 | op, key := s.cfg.Picker.TakeOp() | ||
| @@ -944,7 +941,7 @@ func (s *Supervisor) awaitReady(ctx context.Context, input *terminalInputLifetim | |||||
| 944 | if op.exit { | 941 | if op.exit { | ||
| 945 | // Ctrl+C is the explicit exit. With no attachment to | 942 | // Ctrl+C is the explicit exit. With no attachment to | ||
| 946 | // return to, it is the only close that ends the process. | 943 | // return to, it is the only close that ends the process. | ||
| 947 | - | s.preview.close(s.cfg.Picker) | 944 | + | s.picker.preview.close(s.cfg.Picker) |
| 948 | return readyOutcome{terminated: true} | 945 | return readyOutcome{terminated: true} | ||
| 949 | } | 946 | } | ||
| 950 | // Escape and q cancel; with no attachment there is nothing to | 947 | // Escape and q cancel; with no attachment there is nothing to | ||
| @@ -954,7 +951,7 @@ func (s *Supervisor) awaitReady(ctx context.Context, input *terminalInputLifetim | |||||
| 954 | continue | 951 | continue | ||
| 955 | } | 952 | } | ||
| 956 | if op.commit && key != "" { | 953 | if op.commit && key != "" { | ||
| 957 | - | s.preview.close(s.cfg.Picker) | 954 | + | s.picker.preview.close(s.cfg.Picker) |
| 958 | return readyOutcome{commitKey: key} | 955 | return readyOutcome{commitKey: key} | ||
| 959 | } | 956 | } | ||
| 960 | case <-ctx.Done(): | 957 | case <-ctx.Done(): | ||
| @@ -974,10 +971,10 @@ func (s *Supervisor) refreshPreview(service ports.BrokerNavigator) { | |||||
| 974 | } | 971 | } | ||
| 975 | geometry, err := s.cfg.Terminal.Geometry() | 972 | geometry, err := s.cfg.Terminal.Geometry() | ||
| 976 | if err != nil || !geometry.Valid() { | 973 | if err != nil || !geometry.Valid() { | ||
| 977 | - | s.preview.close(s.cfg.Picker) | 974 | + | s.picker.preview.close(s.cfg.Picker) |
| 978 | return | 975 | return | ||
| 979 | } | 976 | } | ||
| 980 | - | s.preview.refresh(service, s.cfg.Picker, geometry.Size) | 977 | + | s.picker.preview.refresh(service, s.cfg.Picker, geometry.Size) |
| 981 | } | 978 | } | ||
| 982 | 979 | ||||
| 983 | // pickerOps is nil when no picker owns presentation decisions. | 980 | // pickerOps is nil when no picker owns presentation decisions. | ||
| @@ -994,7 +991,7 @@ func (s *Supervisor) pickerOps() <-chan struct{} { | |||||
| 994 | func (s *Supervisor) takePickerClose() bool { | 991 | func (s *Supervisor) takePickerClose() bool { | ||
| 995 | op, key := s.cfg.Picker.TakeOp() | 992 | op, key := s.cfg.Picker.TakeOp() | ||
| 996 | if op.commit && key != "" { | 993 | if op.commit && key != "" { | ||
| 997 | - | s.pendingPickerKey = key | 994 | + | s.picker.pendingPickerKey = key |
| 998 | } | 995 | } | ||
| 999 | if op.close && !op.exit { | 996 | if op.close && !op.exit { | ||
| 1000 | s.notifyPicker(pickerExitHint) | 997 | s.notifyPicker(pickerExitHint) | ||
M internal/usecase/client/supervisor_attach.go +2 -2
| @@ -551,7 +551,7 @@ func (s *Supervisor) runInitialNavigation(ctx context.Context, input *terminalIn | |||
| 551 | 551 | if s.cfg.Picker != nil { | |
| 552 | 552 | s.cfg.Picker.TakeOp() | |
| 553 | 553 | } | |
| 554 | - | s.pendingPickerKey = "" | |
| 554 | + | s.picker.pendingPickerKey = "" | |
| 555 | 555 | return s.runResolvedAttachment(ctx, input, service, pickerAttachmentTarget{request: request}, SessionEnvironmentLocalCLI) | |
| 556 | 556 | } | |
| 557 | 557 | ||
| @@ -901,7 +901,7 @@ settlement: | |||
| 901 | 901 | overlay.resize() | |
| 902 | 902 | case <-notices.arm(s.cfg.Picker): | |
| 903 | 903 | s.renderCurrent() | |
| 904 | - | case outcome := <-s.kills.results(): | |
| 904 | + | case outcome := <-s.picker.kills.results(): | |
| 905 | 905 | s.finishPickerKill(service, outcome) | |
| 906 | 906 | case <-service.Done(): | |
| 907 | 907 | brokerLost = true | |
| @@ -551,7 +551,7 @@ func (s *Supervisor) runInitialNavigation(ctx context.Context, input *terminalIn | |||||
| 551 | if s.cfg.Picker != nil { | 551 | if s.cfg.Picker != nil { | ||
| 552 | s.cfg.Picker.TakeOp() | 552 | s.cfg.Picker.TakeOp() | ||
| 553 | } | 553 | } | ||
| 554 | - | s.pendingPickerKey = "" | 554 | + | s.picker.pendingPickerKey = "" |
| 555 | return s.runResolvedAttachment(ctx, input, service, pickerAttachmentTarget{request: request}, SessionEnvironmentLocalCLI) | 555 | return s.runResolvedAttachment(ctx, input, service, pickerAttachmentTarget{request: request}, SessionEnvironmentLocalCLI) | ||
| 556 | } | 556 | } | ||
| 557 | 557 | ||||
| @@ -901,7 +901,7 @@ settlement: | |||||
| 901 | overlay.resize() | 901 | overlay.resize() | ||
| 902 | case <-notices.arm(s.cfg.Picker): | 902 | case <-notices.arm(s.cfg.Picker): | ||
| 903 | s.renderCurrent() | 903 | s.renderCurrent() | ||
| 904 | - | case outcome := <-s.kills.results(): | 904 | + | case outcome := <-s.picker.kills.results(): |
| 905 | s.finishPickerKill(service, outcome) | 905 | s.finishPickerKill(service, outcome) | ||
| 906 | case <-service.Done(): | 906 | case <-service.Done(): | ||
| 907 | brokerLost = true | 907 | brokerLost = true | ||
M internal/usecase/client/supervisor_overlay.go +4 -4
| @@ -44,7 +44,7 @@ func (o *attachmentPickerOverlay) previewChanged() <-chan struct{} { | |||
| 44 | 44 | if !o.active { | |
| 45 | 45 | return nil | |
| 46 | 46 | } | |
| 47 | - | return o.sup.preview.changed() | |
| 47 | + | return o.sup.picker.preview.changed() | |
| 48 | 48 | } | |
| 49 | 49 | ||
| 50 | 50 | func (o *attachmentPickerOverlay) ops() <-chan struct{} { | |
| @@ -113,7 +113,7 @@ func (o *attachmentPickerOverlay) drop() { | |||
| 113 | 113 | return | |
| 114 | 114 | } | |
| 115 | 115 | o.active = false | |
| 116 | - | o.sup.preview.close(o.picker()) | |
| 116 | + | o.sup.picker.preview.close(o.picker()) | |
| 117 | 117 | o.picker().SetOwnsInput(false) | |
| 118 | 118 | } | |
| 119 | 119 | ||
| @@ -124,7 +124,7 @@ func (o *attachmentPickerOverlay) applyPublication() { | |||
| 124 | 124 | } | |
| 125 | 125 | ||
| 126 | 126 | func (o *attachmentPickerOverlay) publishPreview() { | |
| 127 | - | if o.sup.preview.publish(o.picker()) { | |
| 127 | + | if o.sup.picker.preview.publish(o.picker()) { | |
| 128 | 128 | o.sup.renderCurrent() | |
| 129 | 129 | } | |
| 130 | 130 | } | |
| @@ -181,7 +181,7 @@ func (o *attachmentPickerOverlay) takeOp() { | |||
| 181 | 181 | } | |
| 182 | 182 | s.nav.setSwap(&target) | |
| 183 | 183 | o.swapping = true | |
| 184 | - | s.preview.close(o.picker()) | |
| 184 | + | s.picker.preview.close(o.picker()) | |
| 185 | 185 | s.attachments.requestDetach(o.run.token) | |
| 186 | 186 | case op.kill && key != "": | |
| 187 | 187 | s.startPickerKill(o.service, key) | |
| @@ -44,7 +44,7 @@ func (o *attachmentPickerOverlay) previewChanged() <-chan struct{} { | |||||
| 44 | if !o.active { | 44 | if !o.active { | ||
| 45 | return nil | 45 | return nil | ||
| 46 | } | 46 | } | ||
| 47 | - | return o.sup.preview.changed() | 47 | + | return o.sup.picker.preview.changed() |
| 48 | } | 48 | } | ||
| 49 | 49 | ||||
| 50 | func (o *attachmentPickerOverlay) ops() <-chan struct{} { | 50 | func (o *attachmentPickerOverlay) ops() <-chan struct{} { | ||
| @@ -113,7 +113,7 @@ func (o *attachmentPickerOverlay) drop() { | |||||
| 113 | return | 113 | return | ||
| 114 | } | 114 | } | ||
| 115 | o.active = false | 115 | o.active = false | ||
| 116 | - | o.sup.preview.close(o.picker()) | 116 | + | o.sup.picker.preview.close(o.picker()) |
| 117 | o.picker().SetOwnsInput(false) | 117 | o.picker().SetOwnsInput(false) | ||
| 118 | } | 118 | } | ||
| 119 | 119 | ||||
| @@ -124,7 +124,7 @@ func (o *attachmentPickerOverlay) applyPublication() { | |||||
| 124 | } | 124 | } | ||
| 125 | 125 | ||||
| 126 | func (o *attachmentPickerOverlay) publishPreview() { | 126 | func (o *attachmentPickerOverlay) publishPreview() { | ||
| 127 | - | if o.sup.preview.publish(o.picker()) { | 127 | + | if o.sup.picker.preview.publish(o.picker()) { |
| 128 | o.sup.renderCurrent() | 128 | o.sup.renderCurrent() | ||
| 129 | } | 129 | } | ||
| 130 | } | 130 | } | ||
| @@ -181,7 +181,7 @@ func (o *attachmentPickerOverlay) takeOp() { | |||||
| 181 | } | 181 | } | ||
| 182 | s.nav.setSwap(&target) | 182 | s.nav.setSwap(&target) | ||
| 183 | o.swapping = true | 183 | o.swapping = true | ||
| 184 | - | s.preview.close(o.picker()) | 184 | + | s.picker.preview.close(o.picker()) |
| 185 | s.attachments.requestDetach(o.run.token) | 185 | s.attachments.requestDetach(o.run.token) | ||
| 186 | case op.kill && key != "": | 186 | case op.kill && key != "": | ||
| 187 | s.startPickerKill(o.service, key) | 187 | s.startPickerKill(o.service, key) | ||
M internal/usecase/client/supervisor_picker_kill.go +16 -16
| @@ -69,7 +69,7 @@ func (s *Supervisor) startPickerKill(service ports.BrokerNavigator, key string) | |||
| 69 | 69 | if !ok || key == "" || supervisorNil(service) { | |
| 70 | 70 | return | |
| 71 | 71 | } | |
| 72 | - | if s.kills.running { | |
| 72 | + | if s.picker.kills.running { | |
| 73 | 73 | s.notifyPicker(pickerKillRefused("a kill is already in progress")) | |
| 74 | 74 | return | |
| 75 | 75 | } | |
| @@ -82,17 +82,17 @@ func (s *Supervisor) startPickerKill(service ports.BrokerNavigator, key string) | |||
| 82 | 82 | s.notifyPicker(pickerKillRefused("couldn't kill " + target.name + ": broker unavailable")) | |
| 83 | 83 | return | |
| 84 | 84 | } | |
| 85 | - | if s.kills.done == nil { | |
| 86 | - | s.kills.done = make(chan pickerKillOutcome, 1) | |
| 85 | + | if s.picker.kills.done == nil { | |
| 86 | + | s.picker.kills.done = make(chan pickerKillOutcome, 1) | |
| 87 | 87 | } | |
| 88 | 88 | killCtx, cancel := context.WithCancel(context.Background()) | |
| 89 | - | s.kills.running = true | |
| 90 | - | s.kills.cancel = cancel | |
| 91 | - | s.kills.wg.Add(1) | |
| 89 | + | s.picker.kills.running = true | |
| 90 | + | s.picker.kills.cancel = cancel | |
| 91 | + | s.picker.kills.wg.Add(1) | |
| 92 | 92 | go func() { | |
| 93 | - | defer s.kills.wg.Done() | |
| 93 | + | defer s.picker.kills.wg.Done() | |
| 94 | 94 | result, err := operations.Kill(killCtx, target.route, target.name) | |
| 95 | - | s.kills.done <- pickerKillOutcome{target: target, result: result, err: err} | |
| 95 | + | s.picker.kills.done <- pickerKillOutcome{target: target, result: result, err: err} | |
| 96 | 96 | }() | |
| 97 | 97 | } | |
| 98 | 98 | ||
| @@ -110,22 +110,22 @@ func (s *Supervisor) finishPickerKill(service ports.BrokerNavigator, outcome pic | |||
| 110 | 110 | // retirePickerKill cancels and joins an in-flight kill before its borrowed | |
| 111 | 111 | // service closes. Its outcome is still reported, never replayed. | |
| 112 | 112 | func (s *Supervisor) retirePickerKill() { | |
| 113 | - | if !s.kills.running { | |
| 113 | + | if !s.picker.kills.running { | |
| 114 | 114 | return | |
| 115 | 115 | } | |
| 116 | - | s.kills.cancel() | |
| 117 | - | s.kills.wg.Wait() | |
| 118 | - | outcome := <-s.kills.done | |
| 116 | + | s.picker.kills.cancel() | |
| 117 | + | s.picker.kills.wg.Wait() | |
| 118 | + | outcome := <-s.picker.kills.done | |
| 119 | 119 | s.settlePickerKill() | |
| 120 | 120 | s.notifyPicker(pickerKillNotice(outcome)) | |
| 121 | 121 | } | |
| 122 | 122 | ||
| 123 | 123 | func (s *Supervisor) settlePickerKill() { | |
| 124 | - | if s.kills.cancel != nil { | |
| 125 | - | s.kills.cancel() | |
| 124 | + | if s.picker.kills.cancel != nil { | |
| 125 | + | s.picker.kills.cancel() | |
| 126 | 126 | } | |
| 127 | - | s.kills.running = false | |
| 128 | - | s.kills.cancel = nil | |
| 127 | + | s.picker.kills.running = false | |
| 128 | + | s.picker.kills.cancel = nil | |
| 129 | 129 | } | |
| 130 | 130 | ||
| 131 | 131 | // pickerKillRefused is a kill that was never sent. | |
| @@ -69,7 +69,7 @@ func (s *Supervisor) startPickerKill(service ports.BrokerNavigator, key string) | |||||
| 69 | if !ok || key == "" || supervisorNil(service) { | 69 | if !ok || key == "" || supervisorNil(service) { | ||
| 70 | return | 70 | return | ||
| 71 | } | 71 | } | ||
| 72 | - | if s.kills.running { | 72 | + | if s.picker.kills.running { |
| 73 | s.notifyPicker(pickerKillRefused("a kill is already in progress")) | 73 | s.notifyPicker(pickerKillRefused("a kill is already in progress")) | ||
| 74 | return | 74 | return | ||
| 75 | } | 75 | } | ||
| @@ -82,17 +82,17 @@ func (s *Supervisor) startPickerKill(service ports.BrokerNavigator, key string) | |||||
| 82 | s.notifyPicker(pickerKillRefused("couldn't kill " + target.name + ": broker unavailable")) | 82 | s.notifyPicker(pickerKillRefused("couldn't kill " + target.name + ": broker unavailable")) | ||
| 83 | return | 83 | return | ||
| 84 | } | 84 | } | ||
| 85 | - | if s.kills.done == nil { | 85 | + | if s.picker.kills.done == nil { |
| 86 | - | s.kills.done = make(chan pickerKillOutcome, 1) | 86 | + | s.picker.kills.done = make(chan pickerKillOutcome, 1) |
| 87 | } | 87 | } | ||
| 88 | killCtx, cancel := context.WithCancel(context.Background()) | 88 | killCtx, cancel := context.WithCancel(context.Background()) | ||
| 89 | - | s.kills.running = true | 89 | + | s.picker.kills.running = true |
| 90 | - | s.kills.cancel = cancel | 90 | + | s.picker.kills.cancel = cancel |
| 91 | - | s.kills.wg.Add(1) | 91 | + | s.picker.kills.wg.Add(1) |
| 92 | go func() { | 92 | go func() { | ||
| 93 | - | defer s.kills.wg.Done() | 93 | + | defer s.picker.kills.wg.Done() |
| 94 | result, err := operations.Kill(killCtx, target.route, target.name) | 94 | result, err := operations.Kill(killCtx, target.route, target.name) | ||
| 95 | - | s.kills.done <- pickerKillOutcome{target: target, result: result, err: err} | 95 | + | s.picker.kills.done <- pickerKillOutcome{target: target, result: result, err: err} |
| 96 | }() | 96 | }() | ||
| 97 | } | 97 | } | ||
| 98 | 98 | ||||
| @@ -110,22 +110,22 @@ func (s *Supervisor) finishPickerKill(service ports.BrokerNavigator, outcome pic | |||||
| 110 | // retirePickerKill cancels and joins an in-flight kill before its borrowed | 110 | // retirePickerKill cancels and joins an in-flight kill before its borrowed | ||
| 111 | // service closes. Its outcome is still reported, never replayed. | 111 | // service closes. Its outcome is still reported, never replayed. | ||
| 112 | func (s *Supervisor) retirePickerKill() { | 112 | func (s *Supervisor) retirePickerKill() { | ||
| 113 | - | if !s.kills.running { | 113 | + | if !s.picker.kills.running { |
| 114 | return | 114 | return | ||
| 115 | } | 115 | } | ||
| 116 | - | s.kills.cancel() | 116 | + | s.picker.kills.cancel() |
| 117 | - | s.kills.wg.Wait() | 117 | + | s.picker.kills.wg.Wait() |
| 118 | - | outcome := <-s.kills.done | 118 | + | outcome := <-s.picker.kills.done |
| 119 | s.settlePickerKill() | 119 | s.settlePickerKill() | ||
| 120 | s.notifyPicker(pickerKillNotice(outcome)) | 120 | s.notifyPicker(pickerKillNotice(outcome)) | ||
| 121 | } | 121 | } | ||
| 122 | 122 | ||||
| 123 | func (s *Supervisor) settlePickerKill() { | 123 | func (s *Supervisor) settlePickerKill() { | ||
| 124 | - | if s.kills.cancel != nil { | 124 | + | if s.picker.kills.cancel != nil { |
| 125 | - | s.kills.cancel() | 125 | + | s.picker.kills.cancel() |
| 126 | } | 126 | } | ||
| 127 | - | s.kills.running = false | 127 | + | s.picker.kills.running = false |
| 128 | - | s.kills.cancel = nil | 128 | + | s.picker.kills.cancel = nil |
| 129 | } | 129 | } | ||
| 130 | 130 | ||||
| 131 | // pickerKillRefused is a kill that was never sent. | 131 | // pickerKillRefused is a kill that was never sent. | ||
A internal/usecase/client/supervisor_picker_session.go +15 -0
| @@ -0,0 +1,15 @@ | |||
| 1 | + | package client | |
| 2 | + | ||
| 3 | + | // pickerSession groups the picker state the supervisor carries across its run: | |
| 4 | + | // the commit observed while connecting, the selected row's preview | |
| 5 | + | // subscription, and the runner for the picker's kill operations. Like the | |
| 6 | + | // fields it owns, it is only touched from the run goroutine. The zero value is | |
| 7 | + | // ready once preview has its clock. | |
| 8 | + | type pickerSession struct { | |
| 9 | + | // pendingPickerKey retains a commit observed while connecting. It is | |
| 10 | + | // revalidated against the adopted service just like a ready-phase commit. | |
| 11 | + | pendingPickerKey string | |
| 12 | + | preview previewManager | |
| 13 | + | // kills runs the picker's `x` operations off the run goroutine. | |
| 14 | + | kills pickerKills | |
| 15 | + | } | |
| @@ -0,0 +1,15 @@ | |||||
| 1 | + | package client | |||
| 2 | + | ||||
| 3 | + | // pickerSession groups the picker state the supervisor carries across its run: | |||
| 4 | + | // the commit observed while connecting, the selected row's preview | |||
| 5 | + | // subscription, and the runner for the picker's kill operations. Like the | |||
| 6 | + | // fields it owns, it is only touched from the run goroutine. The zero value is | |||
| 7 | + | // ready once preview has its clock. | |||
| 8 | + | type pickerSession struct { | |||
| 9 | + | // pendingPickerKey retains a commit observed while connecting. It is | |||
| 10 | + | // revalidated against the adopted service just like a ready-phase commit. | |||
| 11 | + | pendingPickerKey string | |||
| 12 | + | preview previewManager | |||
| 13 | + | // kills runs the picker's `x` operations off the run goroutine. | |||
| 14 | + | kills pickerKills | |||
| 15 | + | } | |||