or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

application-framework.mdindex.mdlayout-restoration.mdmime-rendering.mdservice-tokens.mdshell-management.mdstatus-management.mdurl-routing.mdutility-functions.md
tile.json

status-management.mddocs/

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

```