JupyterLab application framework providing the core application class, shell management, plugin system, layout restoration, and routing capabilities.
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
Application state tracking for busy and dirty states with reactive signals, supporting UI feedback for long-running operations and unsaved changes.
Main implementation for application status management providing busy and dirty state tracking with reactive signal support.
/**
* Application status management implementation with reactive signals
*/
class LabStatus implements ILabStatus {
constructor(app: JupyterFrontEnd<any, any>);
/** Signal emitted when application busy state changes */
readonly busySignal: ISignal<JupyterFrontEnd, boolean>;
/** Signal emitted when application dirty state changes */
readonly dirtySignal: ISignal<JupyterFrontEnd, boolean>;
/** Whether the application is currently busy */
readonly isBusy: boolean;
/** Whether the application has unsaved changes */
readonly isDirty: boolean;
/**
* Set the application state to busy
* @returns A disposable used to clear the busy state for the caller
*/
setBusy(): IDisposable;
/**
* Set the application state to dirty
* @returns A disposable used to clear the dirty state for the caller
*/
setDirty(): IDisposable;
}Usage Examples:
import { LabStatus } from "@jupyterlab/application";
import { JupyterFrontEnd } from "@jupyterlab/application";
// Create status manager
const app = new MyJupyterApp();
const status = new LabStatus(app);
// Basic busy state management
async function performAsyncOperation() {
const busyDisposable = status.setBusy();
try {
console.log('Is busy:', status.isBusy); // true
await someAsyncTask();
} finally {
busyDisposable.dispose(); // Clear busy state
console.log('Is busy:', status.isBusy); // false
}
}
// Basic dirty state management
function handleDocumentEdit() {
const dirtyDisposable = status.setDirty();
console.log('Has unsaved changes:', status.isDirty); // true
// Keep disposable until document is saved
return dirtyDisposable;
}
// Listen to status changes
status.busySignal.connect((sender, isBusy) => {
console.log('Busy state changed:', isBusy);
// Update UI - show/hide loading spinner
updateLoadingSpinner(isBusy);
});
status.dirtySignal.connect((sender, isDirty) => {
console.log('Dirty state changed:', isDirty);
// Update UI - show/hide unsaved indicator
updateUnsavedIndicator(isDirty);
});The status system supports multiple callers setting busy/dirty states simultaneously with reference counting.
// Multiple callers can set busy state
const operation1Disposable = status.setBusy();
const operation2Disposable = status.setBusy();
console.log('Is busy:', status.isBusy); // true (2 callers)
// Disposing one doesn't clear busy state
operation1Disposable.dispose();
console.log('Is busy:', status.isBusy); // still true (1 caller remaining)
// Disposing the last caller clears busy state
operation2Disposable.dispose();
console.log('Is busy:', status.isBusy); // false (0 callers)Reference Counting Example:
import { LabStatus } from "@jupyterlab/application";
// Multiple operations can set status simultaneously
class MultiOperationManager {
private status: LabStatus;
private operations: Map<string, IDisposable> = new Map();
constructor(status: LabStatus) {
this.status = status;
}
startOperation(operationId: string): void {
if (!this.operations.has(operationId)) {
const disposable = this.status.setBusy();
this.operations.set(operationId, disposable);
console.log(`Started operation: ${operationId}`);
}
}
finishOperation(operationId: string): void {
const disposable = this.operations.get(operationId);
if (disposable) {
disposable.dispose();
this.operations.delete(operationId);
console.log(`Finished operation: ${operationId}`);
}
}
finishAllOperations(): void {
for (const [id, disposable] of this.operations) {
disposable.dispose();
console.log(`Force finished operation: ${id}`);
}
this.operations.clear();
}
get isAnyOperationRunning(): boolean {
return this.operations.size > 0;
}
}
// Usage
const manager = new MultiOperationManager(status);
// Start multiple operations
manager.startOperation('file-save');
manager.startOperation('model-training');
manager.startOperation('data-processing');
console.log('Any operations running:', manager.isAnyOperationRunning); // true
console.log('Application busy:', status.isBusy); // true
// Finish operations individually
manager.finishOperation('file-save');
console.log('Application busy:', status.isBusy); // still true (2 operations remaining)
manager.finishOperation('model-training');
manager.finishOperation('data-processing');
console.log('Application busy:', status.isBusy); // false (all operations finished)Common patterns for integrating status management with user interface elements.
// UI integration examples
interface StatusUIIntegration {
/** Update loading spinner based on busy state */
updateLoadingSpinner(isBusy: boolean): void;
/** Update document title with unsaved indicator */
updateDocumentTitle(isDirty: boolean): void;
/** Update favicon to show busy/dirty states */
updateFavicon(isBusy: boolean, isDirty: boolean): void;
/** Show/hide global progress bar */
updateProgressBar(isBusy: boolean): void;
}Complete UI Integration Example:
import { LabStatus, ILabStatus } from "@jupyterlab/application";
class StatusUIManager {
private status: ILabStatus;
private loadingElement: HTMLElement;
private titlePrefix: string;
private progressBar: HTMLElement;
constructor(status: ILabStatus) {
this.status = status;
this.titlePrefix = document.title;
this.setupUI();
this.connectSignals();
}
private setupUI(): void {
// Create loading spinner
this.loadingElement = document.createElement('div');
this.loadingElement.className = 'loading-spinner';
this.loadingElement.style.display = 'none';
document.body.appendChild(this.loadingElement);
// Create progress bar
this.progressBar = document.createElement('div');
this.progressBar.className = 'progress-bar';
this.progressBar.style.display = 'none';
document.body.appendChild(this.progressBar);
}
private connectSignals(): void {
// Connect to busy state changes
this.status.busySignal.connect((sender, isBusy) => {
this.updateBusyUI(isBusy);
});
// Connect to dirty state changes
this.status.dirtySignal.connect((sender, isDirty) => {
this.updateDirtyUI(isDirty);
});
}
private updateBusyUI(isBusy: boolean): void {
// Update loading spinner
this.loadingElement.style.display = isBusy ? 'block' : 'none';
// Update progress bar
this.progressBar.style.display = isBusy ? 'block' : 'none';
// Update body class for CSS styling
document.body.classList.toggle('app-busy', isBusy);
// Update cursor
document.body.style.cursor = isBusy ? 'wait' : '';
// Disable interactions during busy state
const interactiveElements = document.querySelectorAll('button, input, select');
interactiveElements.forEach(element => {
(element as HTMLElement).style.pointerEvents = isBusy ? 'none' : '';
});
}
private updateDirtyUI(isDirty: boolean): void {
// Update document title
document.title = isDirty ? `• ${this.titlePrefix}` : this.titlePrefix;
// Update favicon (if you have different favicons)
const favicon = document.querySelector('link[rel="icon"]') as HTMLLinkElement;
if (favicon) {
favicon.href = isDirty ? '/favicon-dirty.ico' : '/favicon.ico';
}
// Update body class for CSS styling
document.body.classList.toggle('app-dirty', isDirty);
// Show unsaved changes indicator
const indicator = document.getElementById('unsaved-indicator');
if (indicator) {
indicator.style.display = isDirty ? 'block' : 'none';
}
}
// Method to get current combined state
getCurrentState(): { isBusy: boolean; isDirty: boolean } {
return {
isBusy: this.status.isBusy,
isDirty: this.status.isDirty
};
}
}
// Usage
const status = new LabStatus(app);
const uiManager = new StatusUIManager(status);
// Check current state
const { isBusy, isDirty } = uiManager.getCurrentState();
console.log('Current state:', { isBusy, isDirty });Sophisticated patterns for complex applications with multiple status types and conditional logic.
// Advanced status management patterns
interface AdvancedStatusManagement {
/** Status with operation categories */
setOperationBusy(category: string, operationId: string): IDisposable;
/** Conditional dirty state based on document type */
setDocumentDirty(documentId: string, isDirty: boolean): IDisposable;
/** Status with priority levels */
setBusyWithPriority(priority: 'low' | 'medium' | 'high'): IDisposable;
}Advanced Status Manager Example:
import { LabStatus, ILabStatus } from "@jupyterlab/application";
import { IDisposable, DisposableDelegate } from "@lumino/disposable";
class AdvancedStatusManager {
private baseStatus: ILabStatus;
private operationCategories: Map<string, Set<string>> = new Map();
private dirtyDocuments: Set<string> = new Set();
private priorityOperations: Map<string, 'low' | 'medium' | 'high'> = new Map();
constructor(baseStatus: ILabStatus) {
this.baseStatus = baseStatus;
}
setOperationBusy(category: string, operationId: string): IDisposable {
// Track operation by category
if (!this.operationCategories.has(category)) {
this.operationCategories.set(category, new Set());
}
this.operationCategories.get(category)!.add(operationId);
// Set busy state
const busyDisposable = this.baseStatus.setBusy();
return new DisposableDelegate(() => {
// Clean up operation tracking
const operations = this.operationCategories.get(category);
if (operations) {
operations.delete(operationId);
if (operations.size === 0) {
this.operationCategories.delete(category);
}
}
// Clear busy state
busyDisposable.dispose();
});
}
setDocumentDirty(documentId: string, isDirty: boolean): IDisposable {
if (isDirty) {
this.dirtyDocuments.add(documentId);
} else {
this.dirtyDocuments.delete(documentId);
}
// Update dirty state based on any dirty documents
const shouldBeDirty = this.dirtyDocuments.size > 0;
const dirtyDisposable = shouldBeDirty ? this.baseStatus.setDirty() : null;
return new DisposableDelegate(() => {
this.dirtyDocuments.delete(documentId);
if (dirtyDisposable) {
dirtyDisposable.dispose();
}
});
}
setBusyWithPriority(priority: 'low' | 'medium' | 'high'): IDisposable {
const operationId = Math.random().toString(36);
this.priorityOperations.set(operationId, priority);
const busyDisposable = this.baseStatus.setBusy();
return new DisposableDelegate(() => {
this.priorityOperations.delete(operationId);
busyDisposable.dispose();
});
}
// Query methods
getOperationsByCategory(category: string): string[] {
return Array.from(this.operationCategories.get(category) || []);
}
getDirtyDocuments(): string[] {
return Array.from(this.dirtyDocuments);
}
getHighestPriorityOperation(): 'low' | 'medium' | 'high' | null {
const priorities = Array.from(this.priorityOperations.values());
if (priorities.includes('high')) return 'high';
if (priorities.includes('medium')) return 'medium';
if (priorities.includes('low')) return 'low';
return null;
}
// Status information
getStatusSummary(): {
totalOperations: number;
operationsByCategory: Record<string, number>;
dirtyDocumentCount: number;
highestPriority: string | null;
} {
const operationsByCategory: Record<string, number> = {};
let totalOperations = 0;
for (const [category, operations] of this.operationCategories) {
const count = operations.size;
operationsByCategory[category] = count;
totalOperations += count;
}
return {
totalOperations,
operationsByCategory,
dirtyDocumentCount: this.dirtyDocuments.size,
highestPriority: this.getHighestPriorityOperation()
};
}
}
// Usage example
const status = new LabStatus(app);
const advancedStatus = new AdvancedStatusManager(status);
// Track operations by category
const saveDisposable = advancedStatus.setOperationBusy('file-operations', 'save-notebook');
const loadDisposable = advancedStatus.setOperationBusy('file-operations', 'load-data');
const trainDisposable = advancedStatus.setOperationBusy('ml-operations', 'train-model');
// Track document dirty states
const doc1Disposable = advancedStatus.setDocumentDirty('notebook1.ipynb', true);
const doc2Disposable = advancedStatus.setDocumentDirty('script.py', true);
// Priority operations
const highPriorityDisposable = advancedStatus.setBusyWithPriority('high');
// Get status summary
const summary = advancedStatus.getStatusSummary();
console.log('Status summary:', summary);
/*
Output:
{
totalOperations: 3,
operationsByCategory: {
'file-operations': 2,
'ml-operations': 1
},
dirtyDocumentCount: 2,
highestPriority: 'high'
}
*/
// Clean up
saveDisposable.dispose();
doc1Disposable.dispose();Patterns for handling errors and ensuring proper cleanup of status states.
// Error recovery patterns
class StatusErrorRecovery {
private status: ILabStatus;
private activeDisposables: Set<IDisposable> = new Set();
constructor(status: ILabStatus) {
this.status = status;
// Set up automatic cleanup on page unload
window.addEventListener('beforeunload', () => {
this.cleanupAll();
});
}
async performOperationWithRecovery<T>(
operation: () => Promise<T>,
category: string = 'default'
): Promise<T> {
const busyDisposable = this.status.setBusy();
this.activeDisposables.add(busyDisposable);
try {
const result = await operation();
return result;
} catch (error) {
console.error(`Operation failed in category ${category}:`, error);
// Could implement retry logic here
throw error;
} finally {
// Always clean up
busyDisposable.dispose();
this.activeDisposables.delete(busyDisposable);
}
}
cleanupAll(): void {
for (const disposable of this.activeDisposables) {
disposable.dispose();
}
this.activeDisposables.clear();
}
get hasActiveOperations(): boolean {
return this.activeDisposables.size > 0;
}
}
// Usage
const errorRecovery = new StatusErrorRecovery(status);
// Safe operation execution
try {
const result = await errorRecovery.performOperationWithRecovery(
async () => {
// Potentially failing operation
const data = await fetchDataFromAPI();
return processData(data);
},
'data-processing'
);
console.log('Operation completed:', result);
} catch (error) {
console.log('Operation failed, but status was cleaned up');
}Integrating status management with browser APIs for enhanced user experience.
// Browser API integration
class BrowserStatusIntegration {
private status: ILabStatus;
constructor(status: ILabStatus) {
this.status = status;
this.setupBrowserIntegration();
}
private setupBrowserIntegration(): void {
// Page visibility API - pause operations when page is hidden
document.addEventListener('visibilitychange', () => {
if (document.hidden && this.status.isBusy) {
console.log('Page hidden during busy operation');
}
});
// Beforeunload - warn about unsaved changes
window.addEventListener('beforeunload', (event) => {
if (this.status.isDirty) {
const message = 'You have unsaved changes. Are you sure you want to leave?';
event.returnValue = message;
return message;
}
});
// Online/offline status
window.addEventListener('online', () => {
console.log('Back online - operations can resume');
});
window.addEventListener('offline', () => {
if (this.status.isBusy) {
console.warn('Went offline during busy operation');
}
});
}
}