Render · Guides

Render Lifecycle Signals

The @threadplane/render library exposes per-context lifecycle signals via the RENDER_LIFECYCLE injection token. All five signals derive from the existing RenderEvent stream via RenderLifecycleService.notify* methods — they share one event source, so there's no double-counting.

Interface

import { InjectionToken, Signal } from '@angular/core';
 
export interface RenderLifecycle {
  /** First mount event in this render context. Sticky — does not reset. */
  readonly firstMountAt: Signal<{ kind: 'spec' | 'element'; elementType?: string; at: number } | null>;
  /** Total mount count since render context started. */
  readonly mountCount: Signal<number>;
  /** Epoch ms of the most recent mount event. */
  readonly lastMountAt: Signal<number | null>;
  /** Epoch ms of the most recent state-change event. */
  readonly lastStateChangeAt: Signal<number | null>;
  /** Most recent handler invocation. */
  readonly lastHandlerInvokedAt: Signal<{ action: string; at: number } | null>;
}
 
export const RENDER_LIFECYCLE = new InjectionToken<RenderLifecycle>('RENDER_LIFECYCLE');

Derivation

All signals derive from RenderEvent (see Events) — RenderLifecycleService subscribes to the stream emitted by RenderSpecComponent and updates the signals:

SignalSource event
firstMountAtfirst RenderLifecycleEvent with event === 'mounted'
mountCountevery RenderLifecycleEvent with event === 'mounted'
lastMountAtevery RenderLifecycleEvent with event === 'mounted'
lastStateChangeAtevery RenderStateChangeEvent
lastHandlerInvokedAtevery RenderHandlerEvent

Subscribing

import { Component, inject, effect } from '@angular/core';
import { RENDER_LIFECYCLE } from '@threadplane/render';
 
@Component({ /* ... */ })
export class MyComponent {
  private lifecycle = inject(RENDER_LIFECYCLE);
 
  constructor() {
    effect(() => {
      const first = this.lifecycle.firstMountAt();
      if (first) {
        console.log('First render at', first.at, 'kind:', first.kind);
      }
    });
  }
}

Reset semantics

firstMountAt is sticky for the life of the render context — once set, it does not reset. The remaining four signals update on every relevant event.

Privacy

These signals contain no spec content, no state values, no handler parameters. They are timestamps, counts, and short discriminants (spec / element, action names from the registered handler bindings) only. The trust contract at libs/telemetry/README.md applies: no app telemetry by default. Subscribing to RENDER_LIFECYCLE in your code does not fire any telemetry; what you do with the signal values is your choice.