@@ -10,9 +10,40 @@ import { ScaleDisplayManager } from './ScaleDisplayManager';
1010import { type FlowStep , renderStep , getStepSummary , type StepRenderContext } from './StepRenderers' ;
1111import { AccordionManager } from './AccordionManager' ;
1212import { BrewProfileRecorder } from './BrewProfileRecorder' ;
13+ import { BrewRunCoordinator } from './BrewRunCoordinator' ;
1314import { NobleInstallModal } from './NobleInstallModal' ;
1415
1516export const VIEW_TYPE_BREWING = 'cubicj-brewing' ;
17+ const TIMER_OPERATION_TIMEOUT_MS = 5000 ;
18+
19+ export function withTimerOperationTimeout ( operation : ( ) => Promise < void > ) : Promise < void > {
20+ return new Promise ( ( resolve , reject ) => {
21+ const rejectWithError = ( reason : unknown ) => {
22+ reject ( reason instanceof Error ? reason : new Error ( String ( reason ) ) ) ;
23+ } ;
24+ const timeoutId = window . setTimeout ( ( ) => {
25+ reject ( new Error ( `Timer operation timed out after ${ TIMER_OPERATION_TIMEOUT_MS } ms` ) ) ;
26+ } , TIMER_OPERATION_TIMEOUT_MS ) ;
27+ let pending : Promise < void > ;
28+ try {
29+ pending = operation ( ) ;
30+ } catch ( err ) {
31+ window . clearTimeout ( timeoutId ) ;
32+ rejectWithError ( err ) ;
33+ return ;
34+ }
35+ pending . then (
36+ ( ) => {
37+ window . clearTimeout ( timeoutId ) ;
38+ resolve ( ) ;
39+ } ,
40+ ( err ) => {
41+ window . clearTimeout ( timeoutId ) ;
42+ rejectWithError ( err ) ;
43+ } ,
44+ ) ;
45+ } ) ;
46+ }
1647
1748export class BrewingView extends ItemView {
1849 private plugin : CubicJBrewingPlugin ;
@@ -29,6 +60,7 @@ export class BrewingView extends ItemView {
2960 private accordion ! : AccordionManager ;
3061
3162 private timerController ! : TimerController ;
63+ private runCoordinator ! : BrewRunCoordinator ;
3264 private recorder = new BrewProfileRecorder ( ) ;
3365
3466 constructor ( leaf : WorkspaceLeaf , plugin : CubicJBrewingPlugin ) {
@@ -57,7 +89,7 @@ export class BrewingView extends ItemView {
5789 const svc = this . plugin . acaiaService ! ;
5890 this . scaleDisplay = new ScaleDisplayManager ( this . scaleConnectBtn , this . scalePowerOffBtn , {
5991 onTimerClick : ( ) => {
60- void this . timerController . handleTimerClick ( ) ;
92+ this . runCoordinator . handleToolbarTimer ( ) ;
6193 } ,
6294 onTare : ( ) => {
6395 void svc . tare ( ) ;
@@ -71,8 +103,16 @@ export class BrewingView extends ItemView {
71103 const scaleElems = this . scaleDisplay . buildData ( dataEl ) ;
72104 this . timerController = new TimerController (
73105 { timerEl : scaleElems . timerEl , timerBtn : scaleElems . timerBtn } ,
74- { startTimer : ( ) => svc . startTimer ( ) , stopTimer : ( ) => svc . stopTimer ( ) , resetTimer : ( ) => svc . resetTimer ( ) } ,
106+ {
107+ startTimer : ( ) => withTimerOperationTimeout ( ( ) => svc . startTimer ( ) ) ,
108+ stopTimer : ( ) => withTimerOperationTimeout ( ( ) => svc . stopTimer ( ) ) ,
109+ resetTimer : ( ) => withTimerOperationTimeout ( ( ) => svc . resetTimer ( ) ) ,
110+ } ,
75111 ) ;
112+ this . runCoordinator = new BrewRunCoordinator ( this . flowState , this . recorder , this . timerController , {
113+ getScaleState : ( ) => this . plugin . acaiaService ?. state ?? 'idle' ,
114+ renderContent : ( focusStep ) => this . renderContent ( focusStep ) ,
115+ } ) ;
76116
77117 const contentArea = container . createDiv ( { cls : 'brewing-content-area' } ) ;
78118 this . accordion = new AccordionManager ( contentArea , {
@@ -111,7 +151,7 @@ export class BrewingView extends ItemView {
111151 }
112152
113153 toggleTimer ( ) : void {
114- void this . timerController . handleTimerClick ( ) ;
154+ this . runCoordinator . handleToolbarTimer ( ) ;
115155 }
116156
117157 powerOff ( ) : void {
@@ -202,14 +242,7 @@ export class BrewingView extends ItemView {
202242 private resetFlow ( ) : void {
203243 this . log ( 'resetFlow' ) ;
204244 this . flowState . cancel ( ) ;
205- this . recorder . reset ( ) ;
206- if ( this . plugin . acaiaService ?. state === 'connected' ) {
207- this . timerController . cancelRun ( ) . catch ( ( err ) => {
208- console . error ( '[BrewingView] resetFlow timer cancel failed:' , err ) ;
209- } ) ;
210- } else {
211- this . timerController . resetToIdle ( ) ;
212- }
245+ this . runCoordinator . resetAll ( ) ;
213246 this . flowState . startBrew ( ) ;
214247 this . accordion . clearExpandedSteps ( ) ;
215248 this . lastFocusedStep = this . flowState . step ;
@@ -229,6 +262,7 @@ export class BrewingView extends ItemView {
229262 updateSummaries : ( ) => this . accordion . updateSummaries ( ) ,
230263 } ,
231264 timerController : this . timerController ,
265+ runCoordinator : this . runCoordinator ,
232266 getWeightText : ( ) => this . scaleDisplay . getWeightText ( ) ,
233267 resetFlow : ( ) => this . resetFlow ( ) ,
234268 recorder : this . recorder ,
@@ -241,8 +275,11 @@ export class BrewingView extends ItemView {
241275 private bindServiceEvents ( ) : void {
242276 this . listen ( 'state' , ( state : AcaiaState ) => {
243277 this . log ( `state → ${ state } ` ) ;
278+ this . runCoordinator . handleScaleState ( state ) ;
244279 this . scaleDisplay . updateHeader ( state , this . plugin . acaiaService ?. scaleName ) ;
245- this . scaleDisplay . updateControls ( state , ( ) => this . timerController . resetToIdle ( ) ) ;
280+ this . scaleDisplay . updateControls ( state , ( ) => {
281+ if ( ! this . runCoordinator . hasActiveRun ( ) ) this . timerController . resetToIdle ( ) ;
282+ } ) ;
246283 } ) ;
247284
248285 this . listen ( 'weight' , ( grams : number , stable : boolean ) => {
@@ -255,7 +292,7 @@ export class BrewingView extends ItemView {
255292 } ) ;
256293
257294 this . listen ( 'button' , ( event : ButtonEvent ) => {
258- this . timerController . handleScaleButton ( event ) ;
295+ this . runCoordinator . handleScaleButton ( event ) ;
259296 } ) ;
260297
261298 this . listen ( 'battery' , ( percent : number ) => {
0 commit comments