CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-jupyterlab--application

JupyterLab application framework providing the core application class, shell management, plugin system, layout restoration, and routing capabilities.

Pending
Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Pending

The risk profile of this skill

Overview
Eval results
Files

status-management.mddocs/

Status Management

Application state tracking for busy and dirty states with reactive signals, supporting UI feedback for long-running operations and unsaved changes.

Capabilities

LabStatus Class

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);
});

Multiple Callers Support

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)

UI Integration Patterns

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 });

Advanced Status Management

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();

Error Recovery and Cleanup

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');
}

Integration with Browser APIs

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');
      }
    });
  }
}

docs

application-framework.md

index.md

layout-restoration.md

mime-rendering.md

service-tokens.md

shell-management.md

status-management.md

url-routing.md

utility-functions.md

tile.json