0
# Status Management
1
2
Application state tracking for busy and dirty states with reactive signals, supporting UI feedback for long-running operations and unsaved changes.
3
4
## Capabilities
5
6
### LabStatus Class
7
8
Main implementation for application status management providing busy and dirty state tracking with reactive signal support.
9
10
```typescript { .api }
11
/**
12
* Application status management implementation with reactive signals
13
*/
14
class LabStatus implements ILabStatus {
15
constructor(app: JupyterFrontEnd<any, any>);
16
17
/** Signal emitted when application busy state changes */
18
readonly busySignal: ISignal<JupyterFrontEnd, boolean>;
19
20
/** Signal emitted when application dirty state changes */
21
readonly dirtySignal: ISignal<JupyterFrontEnd, boolean>;
22
23
/** Whether the application is currently busy */
24
readonly isBusy: boolean;
25
26
/** Whether the application has unsaved changes */
27
readonly isDirty: boolean;
28
29
/**
30
* Set the application state to busy
31
* @returns A disposable used to clear the busy state for the caller
32
*/
33
setBusy(): IDisposable;
34
35
/**
36
* Set the application state to dirty
37
* @returns A disposable used to clear the dirty state for the caller
38
*/
39
setDirty(): IDisposable;
40
}
41
```
42
43
**Usage Examples:**
44
45
```typescript
46
import { LabStatus } from "@jupyterlab/application";
47
import { JupyterFrontEnd } from "@jupyterlab/application";
48
49
// Create status manager
50
const app = new MyJupyterApp();
51
const status = new LabStatus(app);
52
53
// Basic busy state management
54
async function performAsyncOperation() {
55
const busyDisposable = status.setBusy();
56
try {
57
console.log('Is busy:', status.isBusy); // true
58
await someAsyncTask();
59
} finally {
60
busyDisposable.dispose(); // Clear busy state
61
console.log('Is busy:', status.isBusy); // false
62
}
63
}
64
65
// Basic dirty state management
66
function handleDocumentEdit() {
67
const dirtyDisposable = status.setDirty();
68
console.log('Has unsaved changes:', status.isDirty); // true
69
70
// Keep disposable until document is saved
71
return dirtyDisposable;
72
}
73
74
// Listen to status changes
75
status.busySignal.connect((sender, isBusy) => {
76
console.log('Busy state changed:', isBusy);
77
// Update UI - show/hide loading spinner
78
updateLoadingSpinner(isBusy);
79
});
80
81
status.dirtySignal.connect((sender, isDirty) => {
82
console.log('Dirty state changed:', isDirty);
83
// Update UI - show/hide unsaved indicator
84
updateUnsavedIndicator(isDirty);
85
});
86
```
87
88
### Multiple Callers Support
89
90
The status system supports multiple callers setting busy/dirty states simultaneously with reference counting.
91
92
```typescript { .api }
93
// Multiple callers can set busy state
94
const operation1Disposable = status.setBusy();
95
const operation2Disposable = status.setBusy();
96
97
console.log('Is busy:', status.isBusy); // true (2 callers)
98
99
// Disposing one doesn't clear busy state
100
operation1Disposable.dispose();
101
console.log('Is busy:', status.isBusy); // still true (1 caller remaining)
102
103
// Disposing the last caller clears busy state
104
operation2Disposable.dispose();
105
console.log('Is busy:', status.isBusy); // false (0 callers)
106
```
107
108
**Reference Counting Example:**
109
110
```typescript
111
import { LabStatus } from "@jupyterlab/application";
112
113
// Multiple operations can set status simultaneously
114
class MultiOperationManager {
115
private status: LabStatus;
116
private operations: Map<string, IDisposable> = new Map();
117
118
constructor(status: LabStatus) {
119
this.status = status;
120
}
121
122
startOperation(operationId: string): void {
123
if (!this.operations.has(operationId)) {
124
const disposable = this.status.setBusy();
125
this.operations.set(operationId, disposable);
126
console.log(`Started operation: ${operationId}`);
127
}
128
}
129
130
finishOperation(operationId: string): void {
131
const disposable = this.operations.get(operationId);
132
if (disposable) {
133
disposable.dispose();
134
this.operations.delete(operationId);
135
console.log(`Finished operation: ${operationId}`);
136
}
137
}
138
139
finishAllOperations(): void {
140
for (const [id, disposable] of this.operations) {
141
disposable.dispose();
142
console.log(`Force finished operation: ${id}`);
143
}
144
this.operations.clear();
145
}
146
147
get isAnyOperationRunning(): boolean {
148
return this.operations.size > 0;
149
}
150
}
151
152
// Usage
153
const manager = new MultiOperationManager(status);
154
155
// Start multiple operations
156
manager.startOperation('file-save');
157
manager.startOperation('model-training');
158
manager.startOperation('data-processing');
159
160
console.log('Any operations running:', manager.isAnyOperationRunning); // true
161
console.log('Application busy:', status.isBusy); // true
162
163
// Finish operations individually
164
manager.finishOperation('file-save');
165
console.log('Application busy:', status.isBusy); // still true (2 operations remaining)
166
167
manager.finishOperation('model-training');
168
manager.finishOperation('data-processing');
169
console.log('Application busy:', status.isBusy); // false (all operations finished)
170
```
171
172
### UI Integration Patterns
173
174
Common patterns for integrating status management with user interface elements.
175
176
```typescript { .api }
177
// UI integration examples
178
interface StatusUIIntegration {
179
/** Update loading spinner based on busy state */
180
updateLoadingSpinner(isBusy: boolean): void;
181
182
/** Update document title with unsaved indicator */
183
updateDocumentTitle(isDirty: boolean): void;
184
185
/** Update favicon to show busy/dirty states */
186
updateFavicon(isBusy: boolean, isDirty: boolean): void;
187
188
/** Show/hide global progress bar */
189
updateProgressBar(isBusy: boolean): void;
190
}
191
```
192
193
**Complete UI Integration Example:**
194
195
```typescript
196
import { LabStatus, ILabStatus } from "@jupyterlab/application";
197
198
class StatusUIManager {
199
private status: ILabStatus;
200
private loadingElement: HTMLElement;
201
private titlePrefix: string;
202
private progressBar: HTMLElement;
203
204
constructor(status: ILabStatus) {
205
this.status = status;
206
this.titlePrefix = document.title;
207
this.setupUI();
208
this.connectSignals();
209
}
210
211
private setupUI(): void {
212
// Create loading spinner
213
this.loadingElement = document.createElement('div');
214
this.loadingElement.className = 'loading-spinner';
215
this.loadingElement.style.display = 'none';
216
document.body.appendChild(this.loadingElement);
217
218
// Create progress bar
219
this.progressBar = document.createElement('div');
220
this.progressBar.className = 'progress-bar';
221
this.progressBar.style.display = 'none';
222
document.body.appendChild(this.progressBar);
223
}
224
225
private connectSignals(): void {
226
// Connect to busy state changes
227
this.status.busySignal.connect((sender, isBusy) => {
228
this.updateBusyUI(isBusy);
229
});
230
231
// Connect to dirty state changes
232
this.status.dirtySignal.connect((sender, isDirty) => {
233
this.updateDirtyUI(isDirty);
234
});
235
}
236
237
private updateBusyUI(isBusy: boolean): void {
238
// Update loading spinner
239
this.loadingElement.style.display = isBusy ? 'block' : 'none';
240
241
// Update progress bar
242
this.progressBar.style.display = isBusy ? 'block' : 'none';
243
244
// Update body class for CSS styling
245
document.body.classList.toggle('app-busy', isBusy);
246
247
// Update cursor
248
document.body.style.cursor = isBusy ? 'wait' : '';
249
250
// Disable interactions during busy state
251
const interactiveElements = document.querySelectorAll('button, input, select');
252
interactiveElements.forEach(element => {
253
(element as HTMLElement).style.pointerEvents = isBusy ? 'none' : '';
254
});
255
}
256
257
private updateDirtyUI(isDirty: boolean): void {
258
// Update document title
259
document.title = isDirty ? `• ${this.titlePrefix}` : this.titlePrefix;
260
261
// Update favicon (if you have different favicons)
262
const favicon = document.querySelector('link[rel="icon"]') as HTMLLinkElement;
263
if (favicon) {
264
favicon.href = isDirty ? '/favicon-dirty.ico' : '/favicon.ico';
265
}
266
267
// Update body class for CSS styling
268
document.body.classList.toggle('app-dirty', isDirty);
269
270
// Show unsaved changes indicator
271
const indicator = document.getElementById('unsaved-indicator');
272
if (indicator) {
273
indicator.style.display = isDirty ? 'block' : 'none';
274
}
275
}
276
277
// Method to get current combined state
278
getCurrentState(): { isBusy: boolean; isDirty: boolean } {
279
return {
280
isBusy: this.status.isBusy,
281
isDirty: this.status.isDirty
282
};
283
}
284
}
285
286
// Usage
287
const status = new LabStatus(app);
288
const uiManager = new StatusUIManager(status);
289
290
// Check current state
291
const { isBusy, isDirty } = uiManager.getCurrentState();
292
console.log('Current state:', { isBusy, isDirty });
293
```
294
295
### Advanced Status Management
296
297
Sophisticated patterns for complex applications with multiple status types and conditional logic.
298
299
```typescript { .api }
300
// Advanced status management patterns
301
interface AdvancedStatusManagement {
302
/** Status with operation categories */
303
setOperationBusy(category: string, operationId: string): IDisposable;
304
305
/** Conditional dirty state based on document type */
306
setDocumentDirty(documentId: string, isDirty: boolean): IDisposable;
307
308
/** Status with priority levels */
309
setBusyWithPriority(priority: 'low' | 'medium' | 'high'): IDisposable;
310
}
311
```
312
313
**Advanced Status Manager Example:**
314
315
```typescript
316
import { LabStatus, ILabStatus } from "@jupyterlab/application";
317
import { IDisposable, DisposableDelegate } from "@lumino/disposable";
318
319
class AdvancedStatusManager {
320
private baseStatus: ILabStatus;
321
private operationCategories: Map<string, Set<string>> = new Map();
322
private dirtyDocuments: Set<string> = new Set();
323
private priorityOperations: Map<string, 'low' | 'medium' | 'high'> = new Map();
324
325
constructor(baseStatus: ILabStatus) {
326
this.baseStatus = baseStatus;
327
}
328
329
setOperationBusy(category: string, operationId: string): IDisposable {
330
// Track operation by category
331
if (!this.operationCategories.has(category)) {
332
this.operationCategories.set(category, new Set());
333
}
334
this.operationCategories.get(category)!.add(operationId);
335
336
// Set busy state
337
const busyDisposable = this.baseStatus.setBusy();
338
339
return new DisposableDelegate(() => {
340
// Clean up operation tracking
341
const operations = this.operationCategories.get(category);
342
if (operations) {
343
operations.delete(operationId);
344
if (operations.size === 0) {
345
this.operationCategories.delete(category);
346
}
347
}
348
349
// Clear busy state
350
busyDisposable.dispose();
351
});
352
}
353
354
setDocumentDirty(documentId: string, isDirty: boolean): IDisposable {
355
if (isDirty) {
356
this.dirtyDocuments.add(documentId);
357
} else {
358
this.dirtyDocuments.delete(documentId);
359
}
360
361
// Update dirty state based on any dirty documents
362
const shouldBeDirty = this.dirtyDocuments.size > 0;
363
const dirtyDisposable = shouldBeDirty ? this.baseStatus.setDirty() : null;
364
365
return new DisposableDelegate(() => {
366
this.dirtyDocuments.delete(documentId);
367
if (dirtyDisposable) {
368
dirtyDisposable.dispose();
369
}
370
});
371
}
372
373
setBusyWithPriority(priority: 'low' | 'medium' | 'high'): IDisposable {
374
const operationId = Math.random().toString(36);
375
this.priorityOperations.set(operationId, priority);
376
377
const busyDisposable = this.baseStatus.setBusy();
378
379
return new DisposableDelegate(() => {
380
this.priorityOperations.delete(operationId);
381
busyDisposable.dispose();
382
});
383
}
384
385
// Query methods
386
getOperationsByCategory(category: string): string[] {
387
return Array.from(this.operationCategories.get(category) || []);
388
}
389
390
getDirtyDocuments(): string[] {
391
return Array.from(this.dirtyDocuments);
392
}
393
394
getHighestPriorityOperation(): 'low' | 'medium' | 'high' | null {
395
const priorities = Array.from(this.priorityOperations.values());
396
if (priorities.includes('high')) return 'high';
397
if (priorities.includes('medium')) return 'medium';
398
if (priorities.includes('low')) return 'low';
399
return null;
400
}
401
402
// Status information
403
getStatusSummary(): {
404
totalOperations: number;
405
operationsByCategory: Record<string, number>;
406
dirtyDocumentCount: number;
407
highestPriority: string | null;
408
} {
409
const operationsByCategory: Record<string, number> = {};
410
let totalOperations = 0;
411
412
for (const [category, operations] of this.operationCategories) {
413
const count = operations.size;
414
operationsByCategory[category] = count;
415
totalOperations += count;
416
}
417
418
return {
419
totalOperations,
420
operationsByCategory,
421
dirtyDocumentCount: this.dirtyDocuments.size,
422
highestPriority: this.getHighestPriorityOperation()
423
};
424
}
425
}
426
427
// Usage example
428
const status = new LabStatus(app);
429
const advancedStatus = new AdvancedStatusManager(status);
430
431
// Track operations by category
432
const saveDisposable = advancedStatus.setOperationBusy('file-operations', 'save-notebook');
433
const loadDisposable = advancedStatus.setOperationBusy('file-operations', 'load-data');
434
const trainDisposable = advancedStatus.setOperationBusy('ml-operations', 'train-model');
435
436
// Track document dirty states
437
const doc1Disposable = advancedStatus.setDocumentDirty('notebook1.ipynb', true);
438
const doc2Disposable = advancedStatus.setDocumentDirty('script.py', true);
439
440
// Priority operations
441
const highPriorityDisposable = advancedStatus.setBusyWithPriority('high');
442
443
// Get status summary
444
const summary = advancedStatus.getStatusSummary();
445
console.log('Status summary:', summary);
446
/*
447
Output:
448
{
449
totalOperations: 3,
450
operationsByCategory: {
451
'file-operations': 2,
452
'ml-operations': 1
453
},
454
dirtyDocumentCount: 2,
455
highestPriority: 'high'
456
}
457
*/
458
459
// Clean up
460
saveDisposable.dispose();
461
doc1Disposable.dispose();
462
```
463
464
### Error Recovery and Cleanup
465
466
Patterns for handling errors and ensuring proper cleanup of status states.
467
468
```typescript { .api }
469
// Error recovery patterns
470
class StatusErrorRecovery {
471
private status: ILabStatus;
472
private activeDisposables: Set<IDisposable> = new Set();
473
474
constructor(status: ILabStatus) {
475
this.status = status;
476
477
// Set up automatic cleanup on page unload
478
window.addEventListener('beforeunload', () => {
479
this.cleanupAll();
480
});
481
}
482
483
async performOperationWithRecovery<T>(
484
operation: () => Promise<T>,
485
category: string = 'default'
486
): Promise<T> {
487
const busyDisposable = this.status.setBusy();
488
this.activeDisposables.add(busyDisposable);
489
490
try {
491
const result = await operation();
492
return result;
493
} catch (error) {
494
console.error(`Operation failed in category ${category}:`, error);
495
496
// Could implement retry logic here
497
throw error;
498
} finally {
499
// Always clean up
500
busyDisposable.dispose();
501
this.activeDisposables.delete(busyDisposable);
502
}
503
}
504
505
cleanupAll(): void {
506
for (const disposable of this.activeDisposables) {
507
disposable.dispose();
508
}
509
this.activeDisposables.clear();
510
}
511
512
get hasActiveOperations(): boolean {
513
return this.activeDisposables.size > 0;
514
}
515
}
516
517
// Usage
518
const errorRecovery = new StatusErrorRecovery(status);
519
520
// Safe operation execution
521
try {
522
const result = await errorRecovery.performOperationWithRecovery(
523
async () => {
524
// Potentially failing operation
525
const data = await fetchDataFromAPI();
526
return processData(data);
527
},
528
'data-processing'
529
);
530
console.log('Operation completed:', result);
531
} catch (error) {
532
console.log('Operation failed, but status was cleaned up');
533
}
534
```
535
536
## Integration with Browser APIs
537
538
Integrating status management with browser APIs for enhanced user experience.
539
540
```typescript
541
// Browser API integration
542
class BrowserStatusIntegration {
543
private status: ILabStatus;
544
545
constructor(status: ILabStatus) {
546
this.status = status;
547
this.setupBrowserIntegration();
548
}
549
550
private setupBrowserIntegration(): void {
551
// Page visibility API - pause operations when page is hidden
552
document.addEventListener('visibilitychange', () => {
553
if (document.hidden && this.status.isBusy) {
554
console.log('Page hidden during busy operation');
555
}
556
});
557
558
// Beforeunload - warn about unsaved changes
559
window.addEventListener('beforeunload', (event) => {
560
if (this.status.isDirty) {
561
const message = 'You have unsaved changes. Are you sure you want to leave?';
562
event.returnValue = message;
563
return message;
564
}
565
});
566
567
// Online/offline status
568
window.addEventListener('online', () => {
569
console.log('Back online - operations can resume');
570
});
571
572
window.addEventListener('offline', () => {
573
if (this.status.isBusy) {
574
console.warn('Went offline during busy operation');
575
}
576
});
577
}
578
}
579
```