Skip to main content

IdleMonitorService

The IdleMonitorService hooks into Angular’s Zone.js to detect when your application has finished rendering and is idle, signaling to Scully’s Puppeteer instance that the page is ready to be captured.

Overview

This service is critical for Scully’s rendering process. It monitors:
  • Ongoing HTTP requests
  • Asynchronous tasks
  • Zone.js macro tasks
  • Custom rendering completion signals
When all tasks complete and the application goes idle, it dispatches browser events that Scully’s Puppeteer process listens for.

Installation

The service is automatically provided when you import ScullyLibModule:
import { ScullyLibModule } from '@scullyio/ng-lib';

@NgModule({
  imports: [
    ScullyLibModule
  ]
})
export class AppModule { }

How It Works

Automatic Idle Detection

By default, the service automatically monitors Zone.js tasks:
  1. AngularInitialized event is fired when the service initializes
  2. Monitors all macro tasks (especially XMLHttpRequests)
  3. When all tasks complete, waits 250ms for final rendering
  4. Fires AngularReady event, signaling Scully to capture the page
  5. If not idle within 30 seconds, fires AngularTimeout event

Browser Events

The service dispatches three custom events:
// Fired when Angular and the service initialize
new Event('AngularInitialized')

// Fired when the application is idle and ready for capture
new Event('AngularReady')

// Fired if the application doesn't become idle within 30 seconds
new Event('AngularTimeout')

Observable

idle$

idle$: Observable<boolean>
Emits true when the application becomes idle, false when it becomes active again. Example:
import { Component, OnInit } from '@angular/core';
import { IdleMonitorService } from '@scullyio/ng-lib';

@Component({
  selector: 'app-status',
  template: `
    <div class="status" [class.idle]="isIdle">
      {{ isIdle ? 'Application Idle' : 'Processing...' }}
    </div>
  `
})
export class StatusComponent implements OnInit {
  isIdle = false;

  constructor(private idleMonitor: IdleMonitorService) {}

  ngOnInit() {
    this.idleMonitor.idle$.subscribe(idle => {
      this.isIdle = idle;
      console.log('Application idle state:', idle);
    });
  }
}

Methods

fireManualMyAppReadyEvent()

fireManualMyAppReadyEvent(): Promise<boolean>
Manually triggers the AngularReady event. Use this when you have custom loading logic that Zone.js doesn’t capture. Returns: Promise that resolves to true when the event is dispatched. Example:
import { Component, OnInit } from '@angular/core';
import { IdleMonitorService } from '@scullyio/ng-lib';

@Component({
  selector: 'app-custom-loader',
  template: `
    <div *ngIf="loading">Loading custom data...</div>
    <div *ngIf="!loading">{{ data }}</div>
  `
})
export class CustomLoaderComponent implements OnInit {
  loading = true;
  data = '';

  constructor(private idleMonitor: IdleMonitorService) {}

  async ngOnInit() {
    // Custom loading logic that Zone.js doesn't track
    await this.loadExternalData();
    this.loading = false;
    
    // Manually signal that the app is ready
    await this.idleMonitor.fireManualMyAppReadyEvent();
  }
  
  async loadExternalData() {
    // Simulate loading data from external source
    // that doesn't trigger Zone.js
    return new Promise(resolve => {
      setTimeout(() => {
        this.data = 'Custom data loaded';
        resolve(true);
      }, 2000);
    });
  }
}

init()

init(): Promise<boolean>
Returns a promise that resolves when the application first becomes idle. Example:
import { Component, OnInit } from '@angular/core';
import { IdleMonitorService } from '@scullyio/ng-lib';

@Component({
  selector: 'app-root',
  template: `<router-outlet></router-outlet>`
})
export class AppComponent implements OnInit {
  constructor(private idleMonitor: IdleMonitorService) {}

  async ngOnInit() {
    await this.idleMonitor.init();
    console.log('Application has reached idle state');
    // Perform actions after initial idle
  }
}

setPupeteerTimeoutValue()

setPupeteerTimeoutValue(milliseconds: number): void
Sets the timeout value used when Zone.js is not available (fallback mode). Default: 5000ms (5 seconds) Example:
import { Component } from '@angular/core';
import { IdleMonitorService } from '@scullyio/ng-lib';

@Component({
  selector: 'app-root',
  template: `<router-outlet></router-outlet>`
})
export class AppComponent {
  constructor(private idleMonitor: IdleMonitorService) {
    // Increase timeout for slow-loading content
    this.idleMonitor.setPupeteerTimeoutValue(10000); // 10 seconds
  }
}

Configuration

Global Configuration

Configure the service behavior through ScullyLibModule.forRoot():
import { ScullyLibModule } from '@scullyio/ng-lib';

@NgModule({
  imports: [
    ScullyLibModule.forRoot({
      alwaysMonitor: true,  // Always monitor, even outside Scully
      manualIdle: true      // Disable automatic idle detection
    })
  ]
})
export class AppModule { }

Configuration Options

export interface ScullyLibConfig {
  useTransferState?: boolean;  // Enable transfer state (default: true)
  alwaysMonitor?: boolean;     // Monitor in all environments (default: false)
  manualIdle?: boolean;        // Disable automatic monitoring (default: false)
}
alwaysMonitor:
  • false (default): Only monitors during Scully rendering
  • true: Monitors in all environments (useful for debugging)
manualIdle:
  • false (default): Automatic Zone.js monitoring
  • true: You must call fireManualMyAppReadyEvent() manually

Manual Idle Control

Application-Wide Manual Control

Disable automatic detection and take full control:
import { NgModule } from '@angular/core';
import { ScullyLibModule } from '@scullyio/ng-lib';

@NgModule({
  imports: [
    ScullyLibModule.forRoot({
      manualIdle: true
    })
  ]
})
export class AppModule { }
import { Component, OnInit } from '@angular/core';
import { IdleMonitorService } from '@scullyio/ng-lib';

@Component({
  selector: 'app-complex-page',
  template: `
    <div>{{ content }}</div>
  `
})
export class ComplexPageComponent implements OnInit {
  content = 'Loading...';

  constructor(private idleMonitor: IdleMonitorService) {}

  async ngOnInit() {
    // Your custom loading logic
    await this.loadData();
    await this.processData();
    await this.renderVisualization();
    
    // Signal that everything is ready
    await this.idleMonitor.fireManualMyAppReadyEvent();
  }
  
  async loadData() {
    // Load data from multiple sources
  }
  
  async processData() {
    // Process and transform data
  }
  
  async renderVisualization() {
    // Render complex visualizations
    this.content = 'All content loaded!';
  }
}

Per-Route Manual Control

Enable manual idle checking for specific routes:
// scully.config.ts
import { ScullyConfig } from '@scullyio/scully';

export const config: ScullyConfig = {
  routes: {
    '/blog/:slug': {
      type: 'contentFolder',
      slug: {
        folder: './blog'
      }
    },
    '/dashboard/:id': {
      type: 'json',
      manualIdleCheck: true,  // Enable manual idle for this route only
      id: {
        url: 'https://api.example.com/dashboards',
        property: 'id'
      }
    }
  }
};
import { Component, OnInit } from '@angular/core';
import { IdleMonitorService } from '@scullyio/ng-lib';

@Component({
  selector: 'app-dashboard',
  template: `<div>{{ dashboardData }}</div>`
})
export class DashboardComponent implements OnInit {
  dashboardData = '';

  constructor(private idleMonitor: IdleMonitorService) {}

  async ngOnInit() {
    // This route requires manual idle control
    await this.loadDashboardData();
    
    // Wait a bit more for animations to complete
    await new Promise(resolve => setTimeout(resolve, 500));
    
    // Signal ready
    await this.idleMonitor.fireManualMyAppReadyEvent();
  }
  
  async loadDashboardData() {
    // Complex data loading
    this.dashboardData = 'Dashboard loaded';
  }
}

Advanced Usage

Debugging Idle Detection

import { Component, OnInit, OnDestroy } from '@angular/core';
import { IdleMonitorService } from '@scullyio/ng-lib';
import { Subscription } from 'rxjs';

@Component({
  selector: 'app-idle-debugger',
  template: `
    <div class="debug-panel">
      <h3>Idle Monitor Debug</h3>
      <p>Status: {{ isIdle ? 'IDLE' : 'BUSY' }}</p>
      <p>Last change: {{ lastChange | date:'medium' }}</p>
    </div>
  `
})
export class IdleDebuggerComponent implements OnInit, OnDestroy {
  isIdle = false;
  lastChange = new Date();
  private subscription?: Subscription;

  constructor(private idleMonitor: IdleMonitorService) {}

  ngOnInit() {
    // Listen for browser events
    window.addEventListener('AngularInitialized', () => {
      console.log('🚀 Angular Initialized');
    });
    
    window.addEventListener('AngularReady', () => {
      console.log('✅ Angular Ready');
    });
    
    window.addEventListener('AngularTimeout', () => {
      console.warn('⏰ Angular Timeout - took too long to idle');
    });
    
    // Monitor idle state
    this.subscription = this.idleMonitor.idle$.subscribe(idle => {
      this.isIdle = idle;
      this.lastChange = new Date();
      console.log('Idle state changed:', idle);
    });
  }
  
  ngOnDestroy() {
    this.subscription?.unsubscribe();
  }
}

Waiting for External Scripts

import { Component, OnInit } from '@angular/core';
import { IdleMonitorService } from '@scullyio/ng-lib';

declare global {
  interface Window {
    externalLibrary: any;
  }
}

@Component({
  selector: 'app-external-integration',
  template: `<div id="external-widget"></div>`
})
export class ExternalIntegrationComponent implements OnInit {
  constructor(private idleMonitor: IdleMonitorService) {}

  async ngOnInit() {
    // Wait for external script to load
    await this.waitForExternalLibrary();
    
    // Initialize the library
    window.externalLibrary.init('#external-widget');
    
    // Wait for library to finish rendering
    await this.waitForLibraryReady();
    
    // Now signal that Scully can capture
    await this.idleMonitor.fireManualMyAppReadyEvent();
  }
  
  async waitForExternalLibrary(): Promise<void> {
    return new Promise(resolve => {
      const check = () => {
        if (window.externalLibrary) {
          resolve();
        } else {
          setTimeout(check, 100);
        }
      };
      check();
    });
  }
  
  async waitForLibraryReady(): Promise<void> {
    return new Promise(resolve => {
      window.externalLibrary.onReady(() => resolve());
    });
  }
}

Troubleshooting

Content Missing in Rendered Pages

If your content is missing from Scully’s rendered pages:
  1. Enable manual idle control:
ScullyLibModule.forRoot({ manualIdle: true })
  1. Call fireManualMyAppReadyEvent() after content loads:
await this.idleMonitor.fireManualMyAppReadyEvent();

Pages Timing Out

If you see “AngularTimeout” warnings:
  1. Check for infinite observables:
// ❌ Bad - Never completes
interval(1000).subscribe(...);

// ✅ Good - Completes
interval(1000).pipe(take(5)).subscribe(...);
  1. Increase timeout for slow pages:
this.idleMonitor.setPupeteerTimeoutValue(60000); // 60 seconds

Zone.js Not Available

If you see “Scully is using timeouts” warning, add Zone.js polyfills:
// polyfills.ts
import 'zone.js';

Source

View source on GitHub

Build docs developers (and LLMs) love